Ryan Malloy 840bd34f29 🎬 Video Processor v0.4.0 - Complete Multimedia Processing Platform
Professional video processing pipeline with AI analysis, 360° processing,
and adaptive streaming capabilities.

 Core Features:
• AI-powered content analysis with scene detection and quality assessment
• Next-generation codec support (AV1, HEVC, HDR10)
• Adaptive streaming (HLS/DASH) with smart bitrate ladders
• Complete 360° video processing with multiple projection support
• Spatial audio processing (Ambisonic, binaural, object-based)
• Viewport-adaptive streaming with up to 75% bandwidth savings
• Professional testing framework with video-themed HTML dashboards

🏗️ Architecture:
• Modern Python 3.11+ with full type hints
• Pydantic-based configuration with validation
• Async processing with Procrastinate task queue
• Comprehensive test coverage with 11 detailed examples
• Professional documentation structure

🚀 Production Ready:
• MIT License for open source use
• PyPI-ready package metadata
• Docker support for scalable deployment
• Quality assurance with ruff, mypy, and pytest
• Comprehensive example library

From simple encoding to immersive experiences - complete multimedia
processing platform for modern applications.
2025-09-22 01:18:49 -06:00

278 lines
8.5 KiB
Python

"""
Pytest configuration and fixtures for Docker integration tests.
"""
import asyncio
import os
import subprocess
import tempfile
import time
from collections.abc import Generator
from pathlib import Path
from typing import Any
import psycopg2
import pytest
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
import docker
from video_processor.tasks.compat import get_version_info
@pytest.fixture(scope="session")
def docker_client() -> docker.DockerClient:
"""Docker client for managing containers and services."""
return docker.from_env()
@pytest.fixture(scope="session")
def temp_video_dir() -> Generator[Path, None, None]:
"""Temporary directory for test video files."""
with tempfile.TemporaryDirectory(prefix="video_test_") as temp_dir:
yield Path(temp_dir)
@pytest.fixture(scope="session")
def test_suite_manager():
"""Get test suite manager with all video fixtures."""
from tests.fixtures.test_suite_manager import TestSuiteManager
base_dir = Path(__file__).parent.parent / "fixtures" / "videos"
manager = TestSuiteManager(base_dir)
# Ensure test suite is set up
if not (base_dir / "test_suite.json").exists():
manager.setup()
return manager
@pytest.fixture(scope="session")
def test_video_file(test_suite_manager) -> Path:
"""Get a reliable test video from the smoke test suite."""
smoke_videos = test_suite_manager.get_suite_videos("smoke")
# Use the first valid smoke test video
for video_path in smoke_videos:
if video_path.exists() and video_path.stat().st_size > 1000: # At least 1KB
return video_path
# Fallback: generate a simple test video
temp_video = test_suite_manager.base_dir / "temp_test.mp4"
cmd = [
"ffmpeg",
"-y",
"-f",
"lavfi",
"-i",
"testsrc=duration=10:size=640x480:rate=30",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-crf",
"28",
str(temp_video),
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
assert temp_video.exists(), "Test video file was not created"
return temp_video
except (subprocess.CalledProcessError, FileNotFoundError) as e:
pytest.skip(f"FFmpeg not available or failed: {e}")
@pytest.fixture(scope="session")
def docker_compose_project(
docker_client: docker.DockerClient,
) -> Generator[str, None, None]:
"""Start Docker Compose services for testing."""
project_root = Path(__file__).parent.parent.parent
project_name = "video-processor-integration-test"
# Environment variables for test database
test_env = os.environ.copy()
test_env.update(
{
"COMPOSE_PROJECT_NAME": project_name,
"POSTGRES_DB": "video_processor_integration_test",
"DATABASE_URL": "postgresql://video_user:video_password@postgres:5432/video_processor_integration_test",
"PROCRASTINATE_DATABASE_URL": "postgresql://video_user:video_password@postgres:5432/video_processor_integration_test",
}
)
# Start services
print("\n🐳 Starting Docker Compose services for integration tests...")
# First, ensure we're in a clean state
subprocess.run(
["docker-compose", "-p", project_name, "down", "-v", "--remove-orphans"],
cwd=project_root,
env=test_env,
capture_output=True,
)
try:
# Start core services (postgres first)
subprocess.run(
["docker-compose", "-p", project_name, "up", "-d", "postgres"],
cwd=project_root,
env=test_env,
check=True,
)
# Wait for postgres to be healthy
_wait_for_postgres_health(docker_client, project_name)
# Run database migration
subprocess.run(
["docker-compose", "-p", project_name, "run", "--rm", "migrate"],
cwd=project_root,
env=test_env,
check=True,
)
# Start worker service
subprocess.run(
["docker-compose", "-p", project_name, "up", "-d", "worker"],
cwd=project_root,
env=test_env,
check=True,
)
# Wait a moment for services to fully start
time.sleep(5)
print("✅ Docker Compose services started successfully")
yield project_name
finally:
print("\n🧹 Cleaning up Docker Compose services...")
subprocess.run(
["docker-compose", "-p", project_name, "down", "-v", "--remove-orphans"],
cwd=project_root,
env=test_env,
capture_output=True,
)
print("✅ Cleanup completed")
def _wait_for_postgres_health(
client: docker.DockerClient, project_name: str, timeout: int = 30
) -> None:
"""Wait for PostgreSQL container to be healthy."""
container_name = f"{project_name}-postgres-1"
print(f"⏳ Waiting for PostgreSQL container {container_name} to be healthy...")
start_time = time.time()
while time.time() - start_time < timeout:
try:
container = client.containers.get(container_name)
health = container.attrs["State"]["Health"]["Status"]
if health == "healthy":
print("✅ PostgreSQL is healthy")
return
print(f" Health status: {health}")
except docker.errors.NotFound:
print(f" Container {container_name} not found yet...")
except KeyError:
print(" No health check status available yet...")
time.sleep(2)
raise TimeoutError(
f"PostgreSQL container did not become healthy within {timeout} seconds"
)
@pytest.fixture(scope="session")
def postgres_connection(
docker_compose_project: str,
) -> Generator[dict[str, Any], None, None]:
"""PostgreSQL connection parameters for testing."""
conn_params = {
"host": "localhost",
"port": 5432,
"user": "video_user",
"password": "video_password",
"database": "video_processor_integration_test",
}
# Test connection
print("🔌 Testing PostgreSQL connection...")
max_retries = 10
for i in range(max_retries):
try:
with psycopg2.connect(**conn_params) as conn:
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cursor:
cursor.execute("SELECT version();")
version = cursor.fetchone()[0]
print(f"✅ Connected to PostgreSQL: {version}")
break
except psycopg2.OperationalError as e:
if i == max_retries - 1:
raise ConnectionError(
f"Could not connect to PostgreSQL after {max_retries} attempts: {e}"
)
print(f" Attempt {i + 1}/{max_retries} failed, retrying in 2s...")
time.sleep(2)
yield conn_params
@pytest.fixture
def procrastinate_app(postgres_connection: dict[str, Any]):
"""Set up Procrastinate app for testing."""
from video_processor.tasks import setup_procrastinate
db_url = (
f"postgresql://{postgres_connection['user']}:"
f"{postgres_connection['password']}@"
f"{postgres_connection['host']}:{postgres_connection['port']}/"
f"{postgres_connection['database']}"
)
app = setup_procrastinate(db_url)
print(
f"✅ Procrastinate app initialized with {get_version_info()['procrastinate_version']}"
)
return app
@pytest.fixture
def clean_database(postgres_connection: dict[str, Any]):
"""Ensure clean database state for each test."""
print("🧹 Cleaning database state for test...")
with psycopg2.connect(**postgres_connection) as conn:
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cursor:
# Clean up any existing jobs
cursor.execute("""
DELETE FROM procrastinate_jobs WHERE 1=1;
DELETE FROM procrastinate_events WHERE 1=1;
""")
yield
# Cleanup after test
with psycopg2.connect(**postgres_connection) as conn:
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cursor:
cursor.execute("""
DELETE FROM procrastinate_jobs WHERE 1=1;
DELETE FROM procrastinate_events WHERE 1=1;
""")
# Async event loop fixture for async tests
@pytest.fixture
def event_loop():
"""Create an instance of the default event loop for the test session."""
loop = asyncio.new_event_loop()
yield loop
loop.close()