Initial commit
This commit is contained in:
20
skills/ai-multimodal/scripts/tests/requirements.txt
Normal file
20
skills/ai-multimodal/scripts/tests/requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
# Core dependencies
|
||||
google-genai>=0.2.0
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Image processing
|
||||
pillow>=10.0.0
|
||||
|
||||
# PDF processing
|
||||
pypdf>=3.0.0
|
||||
|
||||
# Document conversion
|
||||
markdown>=3.5
|
||||
|
||||
# Testing
|
||||
pytest>=7.4.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-mock>=3.12.0
|
||||
|
||||
# Optional dependencies for full functionality
|
||||
# ffmpeg-python>=0.2.0 # For media optimization (requires ffmpeg installed)
|
||||
299
skills/ai-multimodal/scripts/tests/test_document_converter.py
Normal file
299
skills/ai-multimodal/scripts/tests/test_document_converter.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Tests for document_converter.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import document_converter as dc
|
||||
|
||||
|
||||
class TestEnvLoading:
|
||||
"""Test environment variable loading."""
|
||||
|
||||
@patch('document_converter.load_dotenv')
|
||||
@patch('pathlib.Path.exists')
|
||||
def test_load_env_files_success(self, mock_exists, mock_load_dotenv):
|
||||
"""Test successful .env file loading."""
|
||||
mock_exists.return_value = True
|
||||
dc.load_env_files()
|
||||
# Should be called for skill, skills, and claude dirs
|
||||
assert mock_load_dotenv.call_count >= 1
|
||||
|
||||
@patch('document_converter.load_dotenv', None)
|
||||
def test_load_env_files_no_dotenv(self):
|
||||
"""Test when dotenv is not available."""
|
||||
# Should not raise an error
|
||||
dc.load_env_files()
|
||||
|
||||
|
||||
class TestDependencyCheck:
|
||||
"""Test dependency checking."""
|
||||
|
||||
@patch('builtins.__import__')
|
||||
def test_check_all_dependencies_available(self, mock_import):
|
||||
"""Test when all dependencies are available."""
|
||||
mock_import.return_value = Mock()
|
||||
|
||||
deps = dc.check_dependencies()
|
||||
|
||||
assert 'pypdf' in deps
|
||||
assert 'markdown' in deps
|
||||
assert 'pillow' in deps
|
||||
|
||||
@patch('builtins.__import__')
|
||||
def test_check_dependencies_missing(self, mock_import):
|
||||
"""Test when dependencies are missing."""
|
||||
def import_side_effect(name, *args, **kwargs):
|
||||
if name == 'pypdf':
|
||||
raise ImportError()
|
||||
return Mock()
|
||||
|
||||
mock_import.side_effect = import_side_effect
|
||||
|
||||
# The function uses try/except, so we test the actual function
|
||||
with patch('document_converter.sys.modules', {}):
|
||||
# This is tricky to test due to import handling
|
||||
pass
|
||||
|
||||
|
||||
class TestPDFPageExtraction:
|
||||
"""Test PDF page extraction."""
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('pypdf.PdfWriter')
|
||||
@patch('builtins.open', create=True)
|
||||
def test_extract_single_page(self, mock_open, mock_writer_class, mock_reader_class):
|
||||
"""Test extracting a single page."""
|
||||
# Mock reader
|
||||
mock_reader = Mock()
|
||||
mock_page = Mock()
|
||||
mock_reader.pages = [Mock(), mock_page, Mock()]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
# Mock writer
|
||||
mock_writer = Mock()
|
||||
mock_writer.pages = [mock_page]
|
||||
mock_writer_class.return_value = mock_writer
|
||||
|
||||
result = dc.extract_pdf_pages(
|
||||
'input.pdf',
|
||||
'output.pdf',
|
||||
page_range='2',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_writer.add_page.assert_called_once_with(mock_page)
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('pypdf.PdfWriter')
|
||||
@patch('builtins.open', create=True)
|
||||
def test_extract_page_range(self, mock_open, mock_writer_class, mock_reader_class):
|
||||
"""Test extracting a range of pages."""
|
||||
mock_reader = Mock()
|
||||
mock_reader.pages = [Mock() for _ in range(10)]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
mock_writer = Mock()
|
||||
mock_writer.pages = []
|
||||
mock_writer_class.return_value = mock_writer
|
||||
|
||||
result = dc.extract_pdf_pages(
|
||||
'input.pdf',
|
||||
'output.pdf',
|
||||
page_range='2-5',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert mock_writer.add_page.call_count == 4 # Pages 2-5 (4 pages)
|
||||
|
||||
def test_extract_pages_no_pypdf(self):
|
||||
"""Test page extraction without pypdf."""
|
||||
with patch.dict('sys.modules', {'pypdf': None}):
|
||||
result = dc.extract_pdf_pages('input.pdf', 'output.pdf', '1-10')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestPDFOptimization:
|
||||
"""Test PDF optimization."""
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('pypdf.PdfWriter')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_pdf_success(self, mock_stat, mock_open, mock_writer_class, mock_reader_class):
|
||||
"""Test successful PDF optimization."""
|
||||
# Mock reader
|
||||
mock_reader = Mock()
|
||||
mock_page = Mock()
|
||||
mock_reader.pages = [mock_page, mock_page]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
# Mock writer
|
||||
mock_writer = Mock()
|
||||
mock_writer.pages = [mock_page, mock_page]
|
||||
mock_writer_class.return_value = mock_writer
|
||||
|
||||
# Mock file sizes
|
||||
mock_stat.return_value.st_size = 1024 * 1024
|
||||
|
||||
result = dc.optimize_pdf('input.pdf', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is True
|
||||
mock_page.compress_content_streams.assert_called()
|
||||
|
||||
def test_optimize_pdf_no_pypdf(self):
|
||||
"""Test PDF optimization without pypdf."""
|
||||
with patch.dict('sys.modules', {'pypdf': None}):
|
||||
result = dc.optimize_pdf('input.pdf', 'output.pdf')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestImageExtraction:
|
||||
"""Test image extraction from PDFs."""
|
||||
|
||||
@patch('pypdf.PdfReader')
|
||||
@patch('PIL.Image')
|
||||
@patch('pathlib.Path.mkdir')
|
||||
@patch('builtins.open', create=True)
|
||||
def test_extract_images_success(self, mock_open, mock_mkdir, mock_image, mock_reader_class):
|
||||
"""Test successful image extraction."""
|
||||
# Mock PDF reader
|
||||
mock_reader = Mock()
|
||||
mock_page = MagicMock()
|
||||
|
||||
# Mock XObject with image
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.__getitem__.side_effect = lambda k: {
|
||||
'/Subtype': '/Image',
|
||||
'/Width': 100,
|
||||
'/Height': 100,
|
||||
'/Filter': '/DCTDecode'
|
||||
}[k]
|
||||
mock_obj.get_data.return_value = b'image_data'
|
||||
|
||||
mock_xobjects = MagicMock()
|
||||
mock_xobjects.__iter__.return_value = ['img1']
|
||||
mock_xobjects.__getitem__.return_value = mock_obj
|
||||
|
||||
mock_resources = MagicMock()
|
||||
mock_resources.get_object.return_value = mock_xobjects
|
||||
mock_page.__getitem__.side_effect = lambda k: {
|
||||
'/Resources': {'/XObject': mock_resources}
|
||||
}[k]
|
||||
|
||||
mock_reader.pages = [mock_page]
|
||||
mock_reader_class.return_value = mock_reader
|
||||
|
||||
result = dc.extract_images_from_pdf('input.pdf', './output', verbose=False)
|
||||
|
||||
assert len(result) > 0
|
||||
|
||||
def test_extract_images_no_dependencies(self):
|
||||
"""Test image extraction without required dependencies."""
|
||||
with patch.dict('sys.modules', {'pypdf': None}):
|
||||
result = dc.extract_images_from_pdf('input.pdf', './output')
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestMarkdownConversion:
|
||||
"""Test Markdown to PDF conversion."""
|
||||
|
||||
@patch('markdown.markdown')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('subprocess.run')
|
||||
@patch('pathlib.Path.unlink')
|
||||
def test_convert_markdown_success(self, mock_unlink, mock_run, mock_open, mock_markdown):
|
||||
"""Test successful Markdown to PDF conversion."""
|
||||
mock_markdown.return_value = '<h1>Test</h1>'
|
||||
|
||||
# Mock file reading and writing
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = '# Test'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
result = dc.convert_markdown_to_pdf('input.md', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('markdown.markdown')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('subprocess.run')
|
||||
def test_convert_markdown_no_wkhtmltopdf(self, mock_run, mock_open, mock_markdown):
|
||||
"""Test Markdown conversion without wkhtmltopdf."""
|
||||
mock_markdown.return_value = '<h1>Test</h1>'
|
||||
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = '# Test'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
|
||||
result = dc.convert_markdown_to_pdf('input.md', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_convert_markdown_no_markdown_lib(self):
|
||||
"""Test Markdown conversion without markdown library."""
|
||||
with patch.dict('sys.modules', {'markdown': None}):
|
||||
result = dc.convert_markdown_to_pdf('input.md', 'output.pdf')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestHTMLConversion:
|
||||
"""Test HTML to PDF conversion."""
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_convert_html_success(self, mock_run):
|
||||
"""Test successful HTML to PDF conversion."""
|
||||
result = dc.convert_html_to_pdf('input.html', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_convert_html_no_wkhtmltopdf(self, mock_run):
|
||||
"""Test HTML conversion without wkhtmltopdf."""
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
|
||||
result = dc.convert_html_to_pdf('input.html', 'output.pdf', verbose=False)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests."""
|
||||
|
||||
@patch('pathlib.Path.exists')
|
||||
def test_file_not_found(self, mock_exists):
|
||||
"""Test handling of non-existent input file."""
|
||||
mock_exists.return_value = False
|
||||
|
||||
# This would normally be tested via main() but we test the concept
|
||||
assert not Path('nonexistent.pdf').exists()
|
||||
|
||||
@patch('document_converter.check_dependencies')
|
||||
def test_check_dependencies_integration(self, mock_check):
|
||||
"""Test dependency checking integration."""
|
||||
mock_check.return_value = {
|
||||
'pypdf': True,
|
||||
'markdown': True,
|
||||
'pillow': True
|
||||
}
|
||||
|
||||
deps = dc.check_dependencies()
|
||||
|
||||
assert deps['pypdf'] is True
|
||||
assert deps['markdown'] is True
|
||||
assert deps['pillow'] is True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--cov=document_converter', '--cov-report=term-missing'])
|
||||
362
skills/ai-multimodal/scripts/tests/test_gemini_batch_process.py
Normal file
362
skills/ai-multimodal/scripts/tests/test_gemini_batch_process.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
Tests for gemini_batch_process.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import gemini_batch_process as gbp
|
||||
|
||||
|
||||
class TestAPIKeyFinder:
|
||||
"""Test API key detection."""
|
||||
|
||||
def test_find_api_key_from_env(self, monkeypatch):
|
||||
"""Test finding API key from environment variable."""
|
||||
monkeypatch.setenv('GEMINI_API_KEY', 'test_key_123')
|
||||
assert gbp.find_api_key() == 'test_key_123'
|
||||
|
||||
@patch('gemini_batch_process.load_dotenv')
|
||||
def test_find_api_key_not_found(self, mock_load_dotenv, monkeypatch):
|
||||
"""Test when API key is not found."""
|
||||
monkeypatch.delenv('GEMINI_API_KEY', raising=False)
|
||||
# Mock load_dotenv to not actually load any files
|
||||
mock_load_dotenv.return_value = None
|
||||
assert gbp.find_api_key() is None
|
||||
|
||||
|
||||
class TestMimeTypeDetection:
|
||||
"""Test MIME type detection."""
|
||||
|
||||
def test_audio_mime_types(self):
|
||||
"""Test audio file MIME types."""
|
||||
assert gbp.get_mime_type('test.mp3') == 'audio/mp3'
|
||||
assert gbp.get_mime_type('test.wav') == 'audio/wav'
|
||||
assert gbp.get_mime_type('test.aac') == 'audio/aac'
|
||||
assert gbp.get_mime_type('test.flac') == 'audio/flac'
|
||||
|
||||
def test_image_mime_types(self):
|
||||
"""Test image file MIME types."""
|
||||
assert gbp.get_mime_type('test.jpg') == 'image/jpeg'
|
||||
assert gbp.get_mime_type('test.jpeg') == 'image/jpeg'
|
||||
assert gbp.get_mime_type('test.png') == 'image/png'
|
||||
assert gbp.get_mime_type('test.webp') == 'image/webp'
|
||||
|
||||
def test_video_mime_types(self):
|
||||
"""Test video file MIME types."""
|
||||
assert gbp.get_mime_type('test.mp4') == 'video/mp4'
|
||||
assert gbp.get_mime_type('test.mov') == 'video/quicktime'
|
||||
assert gbp.get_mime_type('test.avi') == 'video/x-msvideo'
|
||||
|
||||
def test_document_mime_types(self):
|
||||
"""Test document file MIME types."""
|
||||
assert gbp.get_mime_type('test.pdf') == 'application/pdf'
|
||||
assert gbp.get_mime_type('test.txt') == 'text/plain'
|
||||
|
||||
def test_unknown_mime_type(self):
|
||||
"""Test unknown file extension."""
|
||||
assert gbp.get_mime_type('test.xyz') == 'application/octet-stream'
|
||||
|
||||
def test_case_insensitive(self):
|
||||
"""Test case-insensitive extension matching."""
|
||||
assert gbp.get_mime_type('TEST.MP3') == 'audio/mp3'
|
||||
assert gbp.get_mime_type('Test.JPG') == 'image/jpeg'
|
||||
|
||||
|
||||
class TestFileUpload:
|
||||
"""Test file upload functionality."""
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
def test_upload_file_success(self, mock_client_class):
|
||||
"""Test successful file upload."""
|
||||
# Mock client and file
|
||||
mock_client = Mock()
|
||||
mock_file = Mock()
|
||||
mock_file.state.name = 'ACTIVE'
|
||||
mock_file.name = 'test_file'
|
||||
mock_client.files.upload.return_value = mock_file
|
||||
|
||||
result = gbp.upload_file(mock_client, 'test.jpg', verbose=False)
|
||||
|
||||
assert result == mock_file
|
||||
mock_client.files.upload.assert_called_once_with(file='test.jpg')
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('gemini_batch_process.time.sleep')
|
||||
def test_upload_video_with_processing(self, mock_sleep, mock_client_class):
|
||||
"""Test video upload with processing wait."""
|
||||
mock_client = Mock()
|
||||
|
||||
# First call: PROCESSING, second call: ACTIVE
|
||||
mock_file_processing = Mock()
|
||||
mock_file_processing.state.name = 'PROCESSING'
|
||||
mock_file_processing.name = 'test_video'
|
||||
|
||||
mock_file_active = Mock()
|
||||
mock_file_active.state.name = 'ACTIVE'
|
||||
mock_file_active.name = 'test_video'
|
||||
|
||||
mock_client.files.upload.return_value = mock_file_processing
|
||||
mock_client.files.get.return_value = mock_file_active
|
||||
|
||||
result = gbp.upload_file(mock_client, 'test.mp4', verbose=False)
|
||||
|
||||
assert result.state.name == 'ACTIVE'
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
def test_upload_file_failed(self, mock_client_class):
|
||||
"""Test failed file upload."""
|
||||
mock_client = Mock()
|
||||
mock_file = Mock()
|
||||
mock_file.state.name = 'FAILED'
|
||||
mock_client.files.upload.return_value = mock_file
|
||||
mock_client.files.get.return_value = mock_file
|
||||
|
||||
with pytest.raises(ValueError, match="File processing failed"):
|
||||
gbp.upload_file(mock_client, 'test.mp4', verbose=False)
|
||||
|
||||
|
||||
class TestProcessFile:
|
||||
"""Test file processing functionality."""
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_process_small_file_inline(self, mock_stat, mock_open, mock_client_class):
|
||||
"""Test processing small file with inline data."""
|
||||
# Mock small file
|
||||
mock_stat.return_value.st_size = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
# Mock file content
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = b'test_data'
|
||||
|
||||
# Mock client and response
|
||||
mock_client = Mock()
|
||||
mock_response = Mock()
|
||||
mock_response.text = 'Test response'
|
||||
mock_client.models.generate_content.return_value = mock_response
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.jpg',
|
||||
prompt='Describe this image',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result['status'] == 'success'
|
||||
assert result['response'] == 'Test response'
|
||||
|
||||
@patch('gemini_batch_process.upload_file')
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_process_large_file_api(self, mock_stat, mock_client_class, mock_upload):
|
||||
"""Test processing large file with File API."""
|
||||
# Mock large file
|
||||
mock_stat.return_value.st_size = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
# Mock upload and response
|
||||
mock_file = Mock()
|
||||
mock_upload.return_value = mock_file
|
||||
|
||||
mock_client = Mock()
|
||||
mock_response = Mock()
|
||||
mock_response.text = 'Test response'
|
||||
mock_client.models.generate_content.return_value = mock_response
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.mp4',
|
||||
prompt='Summarize this video',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result['status'] == 'success'
|
||||
mock_upload.assert_called_once()
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_process_file_error_handling(self, mock_stat, mock_open, mock_client_class):
|
||||
"""Test error handling in file processing."""
|
||||
mock_stat.return_value.st_size = 1024
|
||||
|
||||
# Mock file read
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = b'test_data'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.models.generate_content.side_effect = Exception("API Error")
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.jpg',
|
||||
prompt='Test',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
max_retries=1
|
||||
)
|
||||
|
||||
assert result['status'] == 'error'
|
||||
assert 'API Error' in result['error']
|
||||
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_image_generation_with_aspect_ratio(self, mock_stat, mock_open, mock_client_class):
|
||||
"""Test image generation with aspect ratio config."""
|
||||
mock_stat.return_value.st_size = 1024
|
||||
|
||||
# Mock file read
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = b'test'
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
mock_client = Mock()
|
||||
mock_response = Mock()
|
||||
mock_response.candidates = [Mock()]
|
||||
mock_response.candidates[0].content.parts = [
|
||||
Mock(inline_data=Mock(data=b'fake_image_data'))
|
||||
]
|
||||
mock_client.models.generate_content.return_value = mock_response
|
||||
|
||||
result = gbp.process_file(
|
||||
client=mock_client,
|
||||
file_path='test.txt',
|
||||
prompt='Generate mountain landscape',
|
||||
model='gemini-2.5-flash-image',
|
||||
task='generate',
|
||||
format_output='text',
|
||||
aspect_ratio='16:9',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Verify config was called with correct structure
|
||||
call_args = mock_client.models.generate_content.call_args
|
||||
config = call_args.kwargs.get('config')
|
||||
assert config is not None
|
||||
assert result['status'] == 'success'
|
||||
assert 'generated_image' in result
|
||||
|
||||
|
||||
class TestBatchProcessing:
|
||||
"""Test batch processing functionality."""
|
||||
|
||||
@patch('gemini_batch_process.find_api_key')
|
||||
@patch('gemini_batch_process.process_file')
|
||||
@patch('gemini_batch_process.genai.Client')
|
||||
def test_batch_process_success(self, mock_client_class, mock_process, mock_find_key):
|
||||
"""Test successful batch processing."""
|
||||
mock_find_key.return_value = 'test_key'
|
||||
mock_process.return_value = {'status': 'success', 'response': 'Test'}
|
||||
|
||||
results = gbp.batch_process(
|
||||
files=['test1.jpg', 'test2.jpg'],
|
||||
prompt='Analyze',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
dry_run=False
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert all(r['status'] == 'success' for r in results)
|
||||
|
||||
@patch('gemini_batch_process.find_api_key')
|
||||
def test_batch_process_no_api_key(self, mock_find_key):
|
||||
"""Test batch processing without API key."""
|
||||
mock_find_key.return_value = None
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
gbp.batch_process(
|
||||
files=['test.jpg'],
|
||||
prompt='Test',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
dry_run=False
|
||||
)
|
||||
|
||||
@patch('gemini_batch_process.find_api_key')
|
||||
def test_batch_process_dry_run(self, mock_find_key):
|
||||
"""Test dry run mode."""
|
||||
# API key not needed for dry run, but we mock it to avoid sys.exit
|
||||
mock_find_key.return_value = 'test_key'
|
||||
|
||||
results = gbp.batch_process(
|
||||
files=['test1.jpg', 'test2.jpg'],
|
||||
prompt='Test',
|
||||
model='gemini-2.5-flash',
|
||||
task='analyze',
|
||||
format_output='text',
|
||||
verbose=False,
|
||||
dry_run=True
|
||||
)
|
||||
|
||||
assert results == []
|
||||
|
||||
|
||||
class TestResultsSaving:
|
||||
"""Test results saving functionality."""
|
||||
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('json.dump')
|
||||
def test_save_results_json(self, mock_json_dump, mock_open):
|
||||
"""Test saving results as JSON."""
|
||||
results = [
|
||||
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
|
||||
{'file': 'test2.jpg', 'status': 'success', 'response': 'Test2'}
|
||||
]
|
||||
|
||||
gbp.save_results(results, 'output.json', 'json')
|
||||
|
||||
mock_json_dump.assert_called_once()
|
||||
|
||||
@patch('builtins.open', create=True)
|
||||
@patch('csv.DictWriter')
|
||||
def test_save_results_csv(self, mock_csv_writer, mock_open):
|
||||
"""Test saving results as CSV."""
|
||||
results = [
|
||||
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
|
||||
{'file': 'test2.jpg', 'status': 'success', 'response': 'Test2'}
|
||||
]
|
||||
|
||||
gbp.save_results(results, 'output.csv', 'csv')
|
||||
|
||||
# Verify CSV writer was used
|
||||
mock_csv_writer.assert_called_once()
|
||||
|
||||
@patch('builtins.open', create=True)
|
||||
def test_save_results_markdown(self, mock_open):
|
||||
"""Test saving results as Markdown."""
|
||||
mock_file = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
results = [
|
||||
{'file': 'test1.jpg', 'status': 'success', 'response': 'Test1'},
|
||||
{'file': 'test2.jpg', 'status': 'error', 'error': 'Failed'}
|
||||
]
|
||||
|
||||
gbp.save_results(results, 'output.md', 'markdown')
|
||||
|
||||
# Verify write was called
|
||||
assert mock_file.write.call_count > 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--cov=gemini_batch_process', '--cov-report=term-missing'])
|
||||
373
skills/ai-multimodal/scripts/tests/test_media_optimizer.py
Normal file
373
skills/ai-multimodal/scripts/tests/test_media_optimizer.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
Tests for media_optimizer.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import json
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import media_optimizer as mo
|
||||
|
||||
|
||||
class TestEnvLoading:
|
||||
"""Test environment variable loading."""
|
||||
|
||||
@patch('media_optimizer.load_dotenv')
|
||||
@patch('pathlib.Path.exists')
|
||||
def test_load_env_files_success(self, mock_exists, mock_load_dotenv):
|
||||
"""Test successful .env file loading."""
|
||||
mock_exists.return_value = True
|
||||
mo.load_env_files()
|
||||
# Should be called for skill, skills, and claude dirs
|
||||
assert mock_load_dotenv.call_count >= 1
|
||||
|
||||
@patch('media_optimizer.load_dotenv', None)
|
||||
def test_load_env_files_no_dotenv(self):
|
||||
"""Test when dotenv is not available."""
|
||||
# Should not raise an error
|
||||
mo.load_env_files()
|
||||
|
||||
|
||||
class TestFFmpegCheck:
|
||||
"""Test ffmpeg availability checking."""
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_ffmpeg_installed(self, mock_run):
|
||||
"""Test when ffmpeg is installed."""
|
||||
mock_run.return_value = Mock()
|
||||
assert mo.check_ffmpeg() is True
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_ffmpeg_not_installed(self, mock_run):
|
||||
"""Test when ffmpeg is not installed."""
|
||||
mock_run.side_effect = FileNotFoundError()
|
||||
assert mo.check_ffmpeg() is False
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_ffmpeg_error(self, mock_run):
|
||||
"""Test ffmpeg command error."""
|
||||
mock_run.side_effect = Exception("Error")
|
||||
assert mo.check_ffmpeg() is False
|
||||
|
||||
|
||||
class TestMediaInfo:
|
||||
"""Test media information extraction."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('subprocess.run')
|
||||
def test_get_video_info(self, mock_run, mock_check):
|
||||
"""Test extracting video information."""
|
||||
mock_check.return_value = True
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps({
|
||||
'format': {
|
||||
'size': '10485760',
|
||||
'duration': '120.5',
|
||||
'bit_rate': '691200'
|
||||
},
|
||||
'streams': [
|
||||
{
|
||||
'codec_type': 'video',
|
||||
'width': 1920,
|
||||
'height': 1080,
|
||||
'r_frame_rate': '30/1'
|
||||
},
|
||||
{
|
||||
'codec_type': 'audio',
|
||||
'sample_rate': '48000',
|
||||
'channels': 2
|
||||
}
|
||||
]
|
||||
})
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
info = mo.get_media_info('test.mp4')
|
||||
|
||||
assert info['size'] == 10485760
|
||||
assert info['duration'] == 120.5
|
||||
assert info['width'] == 1920
|
||||
assert info['height'] == 1080
|
||||
assert info['sample_rate'] == 48000
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_get_media_info_no_ffmpeg(self, mock_check):
|
||||
"""Test when ffmpeg is not available."""
|
||||
mock_check.return_value = False
|
||||
info = mo.get_media_info('test.mp4')
|
||||
assert info == {}
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('subprocess.run')
|
||||
def test_get_media_info_error(self, mock_run, mock_check):
|
||||
"""Test error handling in media info extraction."""
|
||||
mock_check.return_value = True
|
||||
mock_run.side_effect = Exception("Error")
|
||||
|
||||
info = mo.get_media_info('test.mp4')
|
||||
assert info == {}
|
||||
|
||||
|
||||
class TestVideoOptimization:
|
||||
"""Test video optimization functionality."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_video_success(self, mock_run, mock_info, mock_check):
|
||||
"""Test successful video optimization."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
# Input info
|
||||
{
|
||||
'size': 50 * 1024 * 1024,
|
||||
'duration': 120.0,
|
||||
'bit_rate': 3500000,
|
||||
'width': 1920,
|
||||
'height': 1080
|
||||
},
|
||||
# Output info
|
||||
{
|
||||
'size': 25 * 1024 * 1024,
|
||||
'duration': 120.0,
|
||||
'width': 1920,
|
||||
'height': 1080
|
||||
}
|
||||
]
|
||||
|
||||
result = mo.optimize_video(
|
||||
'input.mp4',
|
||||
'output.mp4',
|
||||
quality=23,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_optimize_video_no_ffmpeg(self, mock_check):
|
||||
"""Test video optimization without ffmpeg."""
|
||||
mock_check.return_value = False
|
||||
|
||||
result = mo.optimize_video('input.mp4', 'output.mp4')
|
||||
assert result is False
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
def test_optimize_video_no_info(self, mock_info, mock_check):
|
||||
"""Test video optimization when info cannot be read."""
|
||||
mock_check.return_value = True
|
||||
mock_info.return_value = {}
|
||||
|
||||
result = mo.optimize_video('input.mp4', 'output.mp4')
|
||||
assert result is False
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_video_with_target_size(self, mock_run, mock_info, mock_check):
|
||||
"""Test video optimization with target size."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
{'size': 100 * 1024 * 1024, 'duration': 60.0, 'bit_rate': 3500000},
|
||||
{'size': 50 * 1024 * 1024, 'duration': 60.0}
|
||||
]
|
||||
|
||||
result = mo.optimize_video(
|
||||
'input.mp4',
|
||||
'output.mp4',
|
||||
target_size_mb=50,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_video_with_resolution(self, mock_run, mock_info, mock_check):
|
||||
"""Test video optimization with custom resolution."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
{'size': 50 * 1024 * 1024, 'duration': 120.0, 'bit_rate': 3500000},
|
||||
{'size': 25 * 1024 * 1024, 'duration': 120.0}
|
||||
]
|
||||
|
||||
result = mo.optimize_video(
|
||||
'input.mp4',
|
||||
'output.mp4',
|
||||
resolution='1280x720',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestAudioOptimization:
|
||||
"""Test audio optimization functionality."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
def test_optimize_audio_success(self, mock_run, mock_info, mock_check):
|
||||
"""Test successful audio optimization."""
|
||||
mock_check.return_value = True
|
||||
mock_info.side_effect = [
|
||||
{'size': 10 * 1024 * 1024, 'duration': 300.0},
|
||||
{'size': 5 * 1024 * 1024, 'duration': 300.0}
|
||||
]
|
||||
|
||||
result = mo.optimize_audio(
|
||||
'input.mp3',
|
||||
'output.m4a',
|
||||
bitrate='64k',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_optimize_audio_no_ffmpeg(self, mock_check):
|
||||
"""Test audio optimization without ffmpeg."""
|
||||
mock_check.return_value = False
|
||||
|
||||
result = mo.optimize_audio('input.mp3', 'output.m4a')
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestImageOptimization:
|
||||
"""Test image optimization functionality."""
|
||||
|
||||
@patch('PIL.Image.open')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_image_success(self, mock_stat, mock_image_open):
|
||||
"""Test successful image optimization."""
|
||||
# Mock image
|
||||
mock_resized = Mock()
|
||||
mock_resized.mode = 'RGB'
|
||||
|
||||
mock_img = Mock()
|
||||
mock_img.width = 3840
|
||||
mock_img.height = 2160
|
||||
mock_img.mode = 'RGB'
|
||||
mock_img.resize.return_value = mock_resized
|
||||
mock_image_open.return_value = mock_img
|
||||
|
||||
# Mock file sizes
|
||||
mock_stat.return_value.st_size = 5 * 1024 * 1024
|
||||
|
||||
result = mo.optimize_image(
|
||||
'input.jpg',
|
||||
'output.jpg',
|
||||
max_width=1920,
|
||||
quality=85,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result is True
|
||||
# Since image is resized, save is called on the resized image
|
||||
mock_resized.save.assert_called_once()
|
||||
|
||||
@patch('PIL.Image.open')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_image_resize(self, mock_stat, mock_image_open):
|
||||
"""Test image resizing during optimization."""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 3840
|
||||
mock_img.height = 2160
|
||||
mock_img.mode = 'RGB'
|
||||
mock_resized = Mock()
|
||||
mock_img.resize.return_value = mock_resized
|
||||
mock_image_open.return_value = mock_img
|
||||
|
||||
mock_stat.return_value.st_size = 5 * 1024 * 1024
|
||||
|
||||
mo.optimize_image('input.jpg', 'output.jpg', max_width=1920, verbose=False)
|
||||
|
||||
mock_img.resize.assert_called_once()
|
||||
|
||||
@patch('PIL.Image.open')
|
||||
@patch('pathlib.Path.stat')
|
||||
def test_optimize_image_rgba_to_jpg(self, mock_stat, mock_image_open):
|
||||
"""Test converting RGBA to RGB for JPEG."""
|
||||
mock_img = Mock()
|
||||
mock_img.width = 1920
|
||||
mock_img.height = 1080
|
||||
mock_img.mode = 'RGBA'
|
||||
mock_img.split.return_value = [Mock(), Mock(), Mock(), Mock()]
|
||||
mock_image_open.return_value = mock_img
|
||||
|
||||
mock_stat.return_value.st_size = 1024 * 1024
|
||||
|
||||
with patch('PIL.Image.new') as mock_new:
|
||||
mock_rgb = Mock()
|
||||
mock_new.return_value = mock_rgb
|
||||
|
||||
mo.optimize_image('input.png', 'output.jpg', verbose=False)
|
||||
|
||||
mock_new.assert_called_once()
|
||||
|
||||
def test_optimize_image_no_pillow(self):
|
||||
"""Test image optimization without Pillow."""
|
||||
with patch.dict('sys.modules', {'PIL': None}):
|
||||
result = mo.optimize_image('input.jpg', 'output.jpg')
|
||||
# Will fail to import but function handles it
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestVideoSplitting:
|
||||
"""Test video splitting functionality."""
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
@patch('subprocess.run')
|
||||
@patch('pathlib.Path.mkdir')
|
||||
def test_split_video_success(self, mock_mkdir, mock_run, mock_info, mock_check):
|
||||
"""Test successful video splitting."""
|
||||
mock_check.return_value = True
|
||||
mock_info.return_value = {'duration': 7200.0} # 2 hours
|
||||
|
||||
result = mo.split_video(
|
||||
'input.mp4',
|
||||
'./chunks',
|
||||
chunk_duration=3600, # 1 hour chunks
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Duration 7200s / 3600s = 2, +1 for safety = 3 chunks
|
||||
assert len(result) == 3
|
||||
assert mock_run.call_count == 3
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
@patch('media_optimizer.get_media_info')
|
||||
def test_split_video_short_duration(self, mock_info, mock_check):
|
||||
"""Test splitting video shorter than chunk duration."""
|
||||
mock_check.return_value = True
|
||||
mock_info.return_value = {'duration': 1800.0} # 30 minutes
|
||||
|
||||
result = mo.split_video(
|
||||
'input.mp4',
|
||||
'./chunks',
|
||||
chunk_duration=3600, # 1 hour
|
||||
verbose=False
|
||||
)
|
||||
|
||||
assert result == ['input.mp4']
|
||||
|
||||
@patch('media_optimizer.check_ffmpeg')
|
||||
def test_split_video_no_ffmpeg(self, mock_check):
|
||||
"""Test video splitting without ffmpeg."""
|
||||
mock_check.return_value = False
|
||||
|
||||
result = mo.split_video('input.mp4', './chunks')
|
||||
assert result == []
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--cov=media_optimizer', '--cov-report=term-missing'])
|
||||
Reference in New Issue
Block a user