- Introduced `addManagedListener` function to handle event listeners with automatic cleanup. - Updated event listeners for various filter controls to use the new management function. - Added lifecycle signal support for event listeners to ensure proper disposal. - Implemented cleanup logic to remove event listeners when no longer needed. --- Update database schema handling to support async operations - Changed `db.has_table` to `db.ahas_table` for async compatibility. - Updated related database checks to use the new async method. --- Refactor SQLite adapter to prioritize async operations - Transitioned public API to expose async methods as primary. - Removed synchronous methods and replaced them with async counterparts. - Updated internal transaction handling to support async context management. - Enhanced error handling and logging for async operations. --- Add async support for FFProbe tool - Implemented `aread` and `aread_batch` methods for asynchronous metadata reading. - Utilized asyncio subprocess for non-blocking execution of ffprobe commands. --- Enhance IndexScanner with I/O throttling - Introduced `SCAN_IOPS_LIMIT` to control I/O operations during directory scans. - Implemented pacing logic to manage scan I/O operations effectively. --- Update MetadataService to utilize async reading methods - Refactored methods to call async variants of exif and ffprobe reading. - Improved error handling for async operations. --- Implement path resolution checks in asset routes - Added `_is_resolved_path_allowed` function to enforce allowed roots for file paths. - Updated asset route handlers to validate resolved paths against allowed roots. --- Refactor database maintenance routes for async handling - Changed database close operations to use async `aclose` method. - Updated error handling during database maintenance operations. --- Update scan routes to utilize async database vacuuming - Replaced synchronous database vacuum calls with async `avacuum` method. --- Modify tests to support async database operations - Updated test examples to use async context management for database interactions. - Refactored integration tests to ensure proper async cleanup of database connections. - Adjusted regression tests to count async transaction invocations correctly.
156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
"""
|
|
Integration test - validates full service stack.
|
|
Tests: DI, services, metadata extraction on real files.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
import pytest
|
|
|
|
# Fix Windows console encoding
|
|
if sys.platform == "win32":
|
|
import os
|
|
os.system("")
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
|
|
from mjr_am_shared import get_logger, log_success
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_services_build(services):
|
|
"""Test service container builds successfully."""
|
|
logger.info("Testing service container...")
|
|
|
|
# Verify all services present
|
|
assert "db" in services
|
|
assert "exiftool" in services
|
|
assert "ffprobe" in services
|
|
assert "metadata" in services
|
|
assert "health" in services
|
|
|
|
log_success(logger, "Service container built successfully!")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_service(services):
|
|
"""Test health service."""
|
|
logger.info("Testing health service...")
|
|
|
|
health = services["health"]
|
|
|
|
# Get status
|
|
status_result = await health.status()
|
|
assert status_result.ok
|
|
|
|
logger.info(f"Health status: {status_result.data['overall']}")
|
|
logger.info(f"ExifTool available: {status_result.data['tools']['exiftool']['available']}")
|
|
logger.info(f"ffprobe available: {status_result.data['tools']['ffprobe']['available']}")
|
|
|
|
# Get counters
|
|
counters_result = await health.get_counters()
|
|
assert counters_result.ok
|
|
|
|
logger.info(f"Total assets: {counters_result.data.get('total')}")
|
|
|
|
log_success(logger, "Health service works!")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_metadata_service(services):
|
|
"""Test metadata extraction on real files."""
|
|
logger.info("Testing metadata service...")
|
|
|
|
metadata_service = services["metadata"]
|
|
|
|
parser_root = Path(__file__).resolve().parents[1] / "parser"
|
|
|
|
png_files = sorted([p for p in parser_root.rglob("*.png") if p.is_file()])
|
|
webp_files = sorted([p for p in parser_root.rglob("*.webp") if p.is_file()])
|
|
mp4_files = sorted([p for p in parser_root.rglob("*.mp4") if p.is_file()])
|
|
|
|
# Test PNG file
|
|
if png_files:
|
|
png_file = png_files[0]
|
|
logger.info(f"Testing PNG: {png_file.name}")
|
|
result = metadata_service.get_metadata(str(png_file))
|
|
|
|
if result.ok:
|
|
logger.info(f" Quality: {result.meta.get('quality')}")
|
|
logger.info(f" Has workflow: {result.data.get('workflow') is not None}")
|
|
logger.info(f" Has parameters: {result.data.get('parameters') is not None}")
|
|
log_success(logger, "PNG metadata extracted!")
|
|
else:
|
|
logger.warning(f" Failed: {result.error}")
|
|
|
|
# Test WEBP file
|
|
if webp_files:
|
|
webp_file = webp_files[0]
|
|
logger.info(f"Testing WEBP: {webp_file.name}")
|
|
result = metadata_service.get_metadata(str(webp_file))
|
|
|
|
if result.ok:
|
|
logger.info(f" Quality: {result.meta.get('quality')}")
|
|
logger.info(f" Has workflow: {result.data.get('workflow') is not None}")
|
|
log_success(logger, "WEBP metadata extracted!")
|
|
else:
|
|
logger.warning(f" Failed: {result.error}")
|
|
|
|
# Test MP4 file
|
|
if mp4_files:
|
|
mp4_file = mp4_files[0]
|
|
logger.info(f"Testing MP4: {mp4_file.name}")
|
|
result = metadata_service.get_metadata(str(mp4_file))
|
|
|
|
if result.ok:
|
|
logger.info(f" Quality: {result.meta.get('quality')}")
|
|
logger.info(f" Has workflow: {result.data.get('workflow') is not None}")
|
|
logger.info(f" Duration: {result.data.get('duration')}")
|
|
logger.info(f" Resolution: {result.data.get('resolution')}")
|
|
log_success(logger, "MP4 metadata extracted!")
|
|
else:
|
|
logger.warning(f" Failed: {result.error}")
|
|
|
|
def main():
|
|
"""Run integration tests."""
|
|
logger.info("=" * 60)
|
|
logger.info("🚀 Majoor Assets Manager - Integration Test")
|
|
logger.info("=" * 60)
|
|
print()
|
|
|
|
try:
|
|
# Build services (manual run uses direct build)
|
|
from mjr_am_backend.deps import build_services
|
|
import asyncio
|
|
services_res = asyncio.run(build_services("test_integration.db"))
|
|
if not services_res.ok:
|
|
raise RuntimeError(services_res.error or "Failed to build services")
|
|
services = services_res.data
|
|
print()
|
|
|
|
# Test health
|
|
test_health_service(services)
|
|
print()
|
|
|
|
# Test metadata extraction
|
|
test_metadata_service(services)
|
|
print()
|
|
|
|
log_success(logger, "All integration tests passed!")
|
|
logger.info("=" * 60)
|
|
|
|
# Cleanup
|
|
asyncio.run(services["db"].aclose())
|
|
test_db = Path("test_integration.db")
|
|
if test_db.exists():
|
|
test_db.unlink()
|
|
Path("test_integration.db-shm").unlink(missing_ok=True)
|
|
Path("test_integration.db-wal").unlink(missing_ok=True)
|
|
|
|
return 0
|
|
|
|
except Exception as e:
|
|
logger.error(f"Integration test failed: {e}", exc_info=True)
|
|
return 1
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|
|
|