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

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()