mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
423 lines
14 KiB
Python
423 lines
14 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright (c) 2025 Allaun
|
|
|
|
"""Tests for nodupe/tools/video/video_plugin.py - VideoTool."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Import directly from the plugin file to avoid __init__.py import chain issues
|
|
from nodupe.tools.video import video_plugin
|
|
|
|
VideoTool = video_plugin.VideoTool
|
|
register_tool = video_plugin.register_tool
|
|
|
|
|
|
class TestVideoToolProperties:
|
|
"""Test VideoTool properties."""
|
|
|
|
def test_name_property(self):
|
|
"""VideoTool.name returns correct value."""
|
|
tool = VideoTool()
|
|
assert tool.name == "video_tool"
|
|
|
|
def test_version_property(self):
|
|
"""VideoTool.version returns correct value."""
|
|
tool = VideoTool()
|
|
assert tool.version == "1.0.0"
|
|
|
|
def test_dependencies_property(self):
|
|
"""VideoTool.dependencies returns empty list."""
|
|
tool = VideoTool()
|
|
assert tool.dependencies == []
|
|
|
|
|
|
class TestVideoToolInitialization:
|
|
"""Test VideoTool initialization."""
|
|
|
|
def test_init_creates_manager(self):
|
|
"""VideoTool initializes with a manager."""
|
|
tool = VideoTool()
|
|
assert tool.manager is not None
|
|
|
|
def test_api_methods_property(self):
|
|
"""VideoTool.api_methods returns correct methods."""
|
|
tool = VideoTool()
|
|
api_methods = tool.api_methods
|
|
|
|
assert 'extract_frames' in api_methods
|
|
assert 'get_metadata' in api_methods
|
|
assert 'compute_phash' in api_methods
|
|
|
|
# Verify they are bound to manager methods
|
|
assert api_methods['extract_frames'] == tool.manager.extract_frames
|
|
assert api_methods['get_metadata'] == tool.manager.get_video_metadata
|
|
assert api_methods['compute_phash'] == tool.manager.compute_perceptual_hash
|
|
|
|
def test_api_methods_are_callable(self):
|
|
"""VideoTool.api_methods returns callable methods."""
|
|
tool = VideoTool()
|
|
api_methods = tool.api_methods
|
|
|
|
assert callable(api_methods['extract_frames'])
|
|
assert callable(api_methods['get_metadata'])
|
|
assert callable(api_methods['compute_phash'])
|
|
|
|
|
|
class TestVideoToolInitialize:
|
|
"""Test VideoTool.initialize() method."""
|
|
|
|
def test_initialize_registers_service(self):
|
|
"""initialize() registers video_manager service."""
|
|
tool = VideoTool()
|
|
container = MagicMock()
|
|
|
|
tool.initialize(container)
|
|
|
|
container.register_service.assert_called_once_with('video_manager', tool.manager)
|
|
|
|
def test_initialize_with_mock_container(self):
|
|
"""initialize() works with mock container."""
|
|
tool = VideoTool()
|
|
container = MagicMock()
|
|
container.register_service = MagicMock()
|
|
|
|
tool.initialize(container)
|
|
|
|
assert container.register_service.called
|
|
|
|
def test_initialize_preserves_manager(self):
|
|
"""initialize() preserves the manager reference."""
|
|
tool = VideoTool()
|
|
container = MagicMock()
|
|
original_manager = tool.manager
|
|
|
|
tool.initialize(container)
|
|
|
|
assert tool.manager is original_manager
|
|
|
|
|
|
class TestVideoToolShutdown:
|
|
"""Test VideoTool.shutdown() method."""
|
|
|
|
def test_shutdown_no_error(self):
|
|
"""shutdown() completes without error."""
|
|
tool = VideoTool()
|
|
# Should not raise
|
|
tool.shutdown()
|
|
|
|
def test_shutdown_multiple_times(self):
|
|
"""shutdown() can be called multiple times without error."""
|
|
tool = VideoTool()
|
|
tool.shutdown()
|
|
tool.shutdown() # Should not raise
|
|
|
|
def test_shutdown_before_initialize(self):
|
|
"""shutdown() works even if initialize was not called."""
|
|
tool = VideoTool()
|
|
tool.shutdown() # Should not raise
|
|
|
|
|
|
class TestVideoToolGetCapabilities:
|
|
"""Test VideoTool.get_capabilities() method."""
|
|
|
|
def test_get_capabilities_returns_dict(self):
|
|
"""get_capabilities() returns a dictionary with expected keys."""
|
|
tool = VideoTool()
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert isinstance(capabilities, dict)
|
|
assert 'backends' in capabilities
|
|
assert 'available' in capabilities
|
|
|
|
def test_get_capabilities_backends(self):
|
|
"""get_capabilities() returns list for backends."""
|
|
tool = VideoTool()
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert isinstance(capabilities['backends'], list)
|
|
|
|
def test_get_capabilities_available(self):
|
|
"""get_capabilities() returns boolean for available."""
|
|
tool = VideoTool()
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert isinstance(capabilities['available'], bool)
|
|
|
|
def test_get_capabilities_with_mocked_manager(self):
|
|
"""get_capabilities() uses manager info correctly."""
|
|
tool = VideoTool()
|
|
mock_backend1 = MagicMock()
|
|
mock_backend1.__class__.__name__ = 'MockBackend1'
|
|
mock_backend2 = MagicMock()
|
|
mock_backend2.__class__.__name__ = 'MockBackend2'
|
|
tool.manager.backends = [mock_backend1, mock_backend2]
|
|
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert capabilities['backends'] == ['MockBackend1', 'MockBackend2']
|
|
assert capabilities['available'] is True
|
|
|
|
def test_get_capabilities_no_backends(self):
|
|
"""get_capabilities() handles no backends."""
|
|
tool = VideoTool()
|
|
tool.manager.backends = []
|
|
|
|
capabilities = tool.get_capabilities()
|
|
|
|
assert capabilities['backends'] == []
|
|
assert capabilities['available'] is False
|
|
|
|
|
|
class TestRegisterTool:
|
|
"""Test register_tool() function."""
|
|
|
|
def test_register_tool_returns_video_tool(self):
|
|
"""register_tool() returns a VideoTool instance."""
|
|
tool = register_tool()
|
|
assert isinstance(tool, VideoTool)
|
|
|
|
def test_register_tool_creates_new_instance(self):
|
|
"""register_tool() creates a new instance each call."""
|
|
tool1 = register_tool()
|
|
tool2 = register_tool()
|
|
assert tool1 is not tool2
|
|
|
|
def test_register_tool_properties(self):
|
|
"""register_tool() returns tool with correct properties."""
|
|
tool = register_tool()
|
|
assert tool.name == "video_tool"
|
|
assert tool.version == "1.0.0"
|
|
assert tool.dependencies == []
|
|
|
|
|
|
class TestVideoToolDescribeUsage:
|
|
"""Test VideoTool.describe_usage() method."""
|
|
|
|
def test_describe_usage_returns_string(self):
|
|
"""describe_usage() returns a string."""
|
|
tool = VideoTool()
|
|
description = tool.describe_usage()
|
|
|
|
assert isinstance(description, str)
|
|
assert "video" in description.lower()
|
|
|
|
def test_describe_usage_mentions_opencv(self):
|
|
"""describe_usage() mentions OpenCV support."""
|
|
tool = VideoTool()
|
|
description = tool.describe_usage()
|
|
|
|
assert "opencv" in description.lower()
|
|
|
|
def test_describe_usage_mentions_ffmpeg(self):
|
|
"""describe_usage() mentions FFmpeg support."""
|
|
tool = VideoTool()
|
|
description = tool.describe_usage()
|
|
|
|
assert "ffmpeg" in description.lower()
|
|
|
|
def test_describe_usage_mentions_frame_extraction(self):
|
|
"""describe_usage() mentions frame extraction."""
|
|
tool = VideoTool()
|
|
description = tool.describe_usage()
|
|
|
|
assert "frame" in description.lower() or "extraction" in description.lower()
|
|
|
|
def test_describe_usage_mentions_metadata(self):
|
|
"""describe_usage() mentions metadata analysis."""
|
|
tool = VideoTool()
|
|
description = tool.describe_usage()
|
|
|
|
assert "metadata" in description.lower()
|
|
|
|
def test_describe_usage_mentions_hashing(self):
|
|
"""describe_usage() mentions perceptual hashing."""
|
|
tool = VideoTool()
|
|
description = tool.describe_usage()
|
|
|
|
assert "hash" in description.lower()
|
|
|
|
|
|
class TestVideoToolRunStandalone:
|
|
"""Test VideoTool.run_standalone() method."""
|
|
|
|
def test_run_standalone_returns_zero(self, capsys):
|
|
"""run_standalone() returns 0 and prints output."""
|
|
tool = VideoTool()
|
|
result = tool.run_standalone([])
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "Video Tool: Self-test mode." in captured.out
|
|
assert "Backends:" in captured.out
|
|
assert "Available:" in captured.out
|
|
|
|
def test_run_standalone_with_args(self, capsys):
|
|
"""run_standalone() handles args parameter."""
|
|
tool = VideoTool()
|
|
result = tool.run_standalone(['--verbose', '--test'])
|
|
|
|
assert result == 0
|
|
|
|
def test_run_standalone_empty_args(self, capsys):
|
|
"""run_standalone() works with empty args."""
|
|
tool = VideoTool()
|
|
result = tool.run_standalone([])
|
|
|
|
assert result == 0
|
|
captured = capsys.readouterr()
|
|
assert "Video Tool: Self-test mode." in captured.out
|
|
|
|
def test_run_standalone_with_various_args(self, capsys):
|
|
"""run_standalone() handles various argument formats."""
|
|
tool = VideoTool()
|
|
|
|
# Test with different argument formats
|
|
for args in [['--help'], ['-v'], ['test', 'arg1', 'arg2'], []]:
|
|
result = tool.run_standalone(args)
|
|
assert result == 0
|
|
|
|
|
|
class TestVideoToolWithMockedManager:
|
|
"""Test VideoTool with mocked manager for complete coverage."""
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_with_mocked_opencv_backend(self, mock_get_manager):
|
|
"""Test with mocked OpenCV backend."""
|
|
mock_manager = MagicMock()
|
|
mock_backend = MagicMock()
|
|
mock_backend.__class__.__name__ = 'OpenCVBackend'
|
|
mock_manager.backends = [mock_backend]
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
caps = tool.get_capabilities()
|
|
assert caps['backends'] == ['OpenCVBackend']
|
|
assert caps['available'] is True
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_with_mocked_ffmpeg_backend(self, mock_get_manager):
|
|
"""Test with mocked FFmpeg backend."""
|
|
mock_manager = MagicMock()
|
|
mock_backend = MagicMock()
|
|
mock_backend.__class__.__name__ = 'FFmpegSubprocessBackend'
|
|
mock_manager.backends = [mock_backend]
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
caps = tool.get_capabilities()
|
|
assert caps['backends'] == ['FFmpegSubprocessBackend']
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_with_multiple_backends(self, mock_get_manager):
|
|
"""Test with multiple backends."""
|
|
mock_manager = MagicMock()
|
|
mock_backend1 = MagicMock()
|
|
mock_backend1.__class__.__name__ = 'OpenCVBackend'
|
|
mock_backend2 = MagicMock()
|
|
mock_backend2.__class__.__name__ = 'FFmpegSubprocessBackend'
|
|
mock_manager.backends = [mock_backend1, mock_backend2]
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
caps = tool.get_capabilities()
|
|
assert 'OpenCVBackend' in caps['backends']
|
|
assert 'FFmpegSubprocessBackend' in caps['backends']
|
|
assert caps['available'] is True
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_api_methods_call_manager(self, mock_get_manager):
|
|
"""Test that api_methods correctly call manager methods."""
|
|
mock_manager = MagicMock()
|
|
mock_manager.backends = [MagicMock()]
|
|
mock_manager.extract_frames.return_value = []
|
|
mock_manager.get_video_metadata.return_value = {'width': 1920, 'height': 1080}
|
|
mock_manager.compute_perceptual_hash.return_value = 'abc123'
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
# Test extract_frames
|
|
tool.api_methods['extract_frames']('video.mp4')
|
|
mock_manager.extract_frames.assert_called_once()
|
|
|
|
# Test get_metadata
|
|
tool.api_methods['get_metadata']('video.mp4')
|
|
mock_manager.get_video_metadata.assert_called_once()
|
|
|
|
# Test compute_phash
|
|
import numpy as np
|
|
frame = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
tool.api_methods['compute_phash'](frame)
|
|
mock_manager.compute_perceptual_hash.assert_called_once()
|
|
|
|
|
|
class TestVideoToolEdgeCases:
|
|
"""Test VideoTool edge cases."""
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_manager_raises_exception(self, mock_get_manager):
|
|
"""Test handling when manager raises exception."""
|
|
mock_manager = MagicMock()
|
|
mock_manager.backends = [MagicMock()]
|
|
mock_manager.extract_frames.side_effect = Exception("Extraction error")
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
with pytest.raises(Exception, match="Extraction error"):
|
|
tool.manager.extract_frames('video.mp4')
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_manager_returns_empty_results(self, mock_get_manager):
|
|
"""Test handling when manager operations return empty results."""
|
|
mock_manager = MagicMock()
|
|
mock_manager.backends = [MagicMock()]
|
|
mock_manager.extract_frames.return_value = []
|
|
mock_manager.get_video_metadata.return_value = {}
|
|
mock_manager.compute_perceptual_hash.return_value = ''
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
assert tool.manager.extract_frames('video.mp4') == []
|
|
assert tool.manager.get_video_metadata('video.mp4') == {}
|
|
assert tool.manager.compute_perceptual_hash(None) == ''
|
|
|
|
def test_tool_instantiation_multiple_times(self):
|
|
"""Test that multiple tool instances can be created."""
|
|
tool1 = VideoTool()
|
|
tool2 = VideoTool()
|
|
|
|
assert tool1 is not tool2
|
|
assert tool1.name == tool2.name
|
|
assert tool1.version == tool2.version
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_manager_no_backends(self, mock_get_manager):
|
|
"""Test handling when manager has no backends."""
|
|
mock_manager = MagicMock()
|
|
mock_manager.backends = []
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
caps = tool.get_capabilities()
|
|
assert caps['backends'] == []
|
|
assert caps['available'] is False
|
|
|
|
@patch('nodupe.tools.video.video_plugin.get_video_backend_manager')
|
|
def test_manager_backends_attribute_error(self, mock_get_manager):
|
|
"""Test handling when manager backends attribute is missing."""
|
|
mock_manager = MagicMock()
|
|
del mock_manager.backends
|
|
mock_get_manager.return_value = mock_manager
|
|
|
|
tool = VideoTool()
|
|
|
|
with pytest.raises(AttributeError):
|
|
tool.get_capabilities()
|