video-processor/tests/fixtures/test_suite_manager.py
Ryan Malloy 90508c417d Implement comprehensive test video suite with fixtures and integration
- Created comprehensive test video downloader (CC-licensed content)
- Built synthetic video generator for edge cases, codecs, patterns
- Added test suite manager with categorized test suites (smoke, basic, codecs, edge_cases, stress)
- Generated 108+ test videos covering various scenarios
- Updated integration tests to use comprehensive test suite
- Added comprehensive video processing integration tests
- Validated test suite structure and accessibility

Test Results:
- Generated 99 valid test videos (9 invalid by design)
- Successfully created edge cases: single frame, unusual resolutions, high FPS
- Multiple codec support: H.264, H.265, VP8, VP9, Theora, MPEG4
- Audio variations: mono/stereo, different sample rates, no audio, audio-only
- Visual patterns: SMPTE bars, RGB test, YUV test, checkerboard
- Motion tests: rotation, camera shake, scene changes
- Stress tests: high complexity scenes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 12:32:20 -06:00

243 lines
8.6 KiB
Python

"""
Manage the complete test video suite.
"""
import json
import shutil
from pathlib import Path
from typing import Dict, List, Optional
import subprocess
import hashlib
class TestSuiteManager:
"""Manage test video suite with categorization and validation."""
def __init__(self, base_dir: Path):
self.base_dir = Path(base_dir)
self.opensource_dir = self.base_dir / "opensource"
self.synthetic_dir = self.base_dir / "synthetic"
self.custom_dir = self.base_dir / "custom"
# Test categories
self.categories = {
"smoke": "Quick smoke tests (< 5 videos)",
"basic": "Basic functionality tests",
"codecs": "Codec-specific tests",
"edge_cases": "Edge cases and boundary conditions",
"stress": "Stress and performance tests",
"regression": "Regression test suite",
"full": "Complete test suite"
}
# Test suites
self.suites = {
"smoke": [
"opensource/standard/big_buck_bunny_1080p_30fps.mp4",
"synthetic/patterns/smpte_bars.mp4",
"synthetic/edge_cases/single_frame.mp4",
],
"basic": [
"opensource/standard/*.mp4",
"opensource/resolutions/*.mp4",
"synthetic/patterns/*.mp4",
],
"codecs": [
"synthetic/codecs/*.webm",
"synthetic/codecs/*.ogv",
"synthetic/codecs/*.mp4",
],
"edge_cases": [
"synthetic/edge_cases/*.mp4",
"synthetic/audio/no_audio.mp4",
"synthetic/audio/audio_only.mp4",
],
"stress": [
"synthetic/stress/*.mp4",
"synthetic/motion/fast_*.mp4",
],
}
def setup(self):
"""Set up the complete test suite."""
print("🔧 Setting up test video suite...")
# Create directories
for dir_path in [self.opensource_dir, self.synthetic_dir, self.custom_dir]:
dir_path.mkdir(parents=True, exist_ok=True)
# Download open source videos
try:
from download_test_videos import TestVideoDownloader
downloader = TestVideoDownloader(self.opensource_dir)
downloader.download_all()
except Exception as e:
print(f"⚠ Failed to download opensource videos: {e}")
# Generate synthetic videos
try:
from generate_synthetic_videos import SyntheticVideoGenerator
generator = SyntheticVideoGenerator(self.synthetic_dir)
generator.generate_all()
except Exception as e:
print(f"⚠ Failed to generate synthetic videos: {e}")
# Validate suite
self.validate()
# Generate test configuration
self.generate_config()
print("✅ Test suite setup complete!")
def validate(self):
"""Validate all test videos are accessible and valid."""
print("\n🔍 Validating test suite...")
invalid_files = []
valid_count = 0
for ext in ["*.mp4", "*.webm", "*.ogv", "*.mkv", "*.avi"]:
for video_file in self.base_dir.rglob(ext):
if self.validate_video(video_file):
valid_count += 1
else:
invalid_files.append(video_file)
print(f" ✓ Valid videos: {valid_count}")
if invalid_files:
print(f" ✗ Invalid videos: {len(invalid_files)}")
for f in invalid_files[:5]: # Show first 5
print(f" - {f.relative_to(self.base_dir)}")
return len(invalid_files) == 0
def validate_video(self, video_path: Path) -> bool:
"""Validate a single video file."""
try:
result = subprocess.run(
['ffprobe', '-v', 'error', str(video_path)],
capture_output=True,
timeout=5
)
return result.returncode == 0
except:
return False
def generate_config(self):
"""Generate test configuration file."""
config = {
"base_dir": str(self.base_dir),
"categories": self.categories,
"suites": {},
"videos": {}
}
# Expand suite patterns
for suite_name, patterns in self.suites.items():
suite_files = []
for pattern in patterns:
if '*' in pattern:
# Glob pattern
for f in self.base_dir.glob(pattern):
if f.is_file():
suite_files.append(str(f.relative_to(self.base_dir)))
else:
# Specific file
f = self.base_dir / pattern
if f.exists():
suite_files.append(pattern)
config["suites"][suite_name] = sorted(set(suite_files))
# Catalog all videos
for ext in ["*.mp4", "*.webm", "*.ogv", "*.mkv", "*.avi"]:
for video_file in self.base_dir.rglob(ext):
rel_path = str(video_file.relative_to(self.base_dir))
config["videos"][rel_path] = {
"size_mb": video_file.stat().st_size / 1024 / 1024,
"hash": self.get_file_hash(video_file)
}
# Save configuration
config_path = self.base_dir / "test_suite.json"
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
print(f"\n📋 Test configuration saved to: {config_path}")
# Print summary
print("\n📊 Test Suite Summary:")
for suite_name, files in config["suites"].items():
print(f" {suite_name}: {len(files)} videos")
print(f" Total: {len(config['videos'])} videos")
total_size = sum(v["size_mb"] for v in config["videos"].values())
print(f" Total size: {total_size:.1f} MB")
def get_file_hash(self, file_path: Path) -> str:
"""Get SHA256 hash of file (first 1MB for speed)."""
hasher = hashlib.sha256()
with open(file_path, 'rb') as f:
hasher.update(f.read(1024 * 1024)) # First 1MB
return hasher.hexdigest()[:16] # Short hash
def get_suite_videos(self, suite_name: str) -> List[Path]:
"""Get list of videos for a specific test suite."""
config_path = self.base_dir / "test_suite.json"
if not config_path.exists():
self.generate_config()
with open(config_path, 'r') as f:
config = json.load(f)
if suite_name not in config["suites"]:
raise ValueError(f"Unknown suite: {suite_name}")
return [self.base_dir / p for p in config["suites"][suite_name]]
def cleanup(self, keep_suite: Optional[str] = None):
"""Clean up test videos, optionally keeping specific suite."""
if keep_suite:
# Get videos to keep
keep_videos = set(self.get_suite_videos(keep_suite))
# Remove others
for ext in ["*.mp4", "*.webm", "*.ogv"]:
for video_file in self.base_dir.rglob(ext):
if video_file not in keep_videos:
video_file.unlink()
print(f"✓ Cleaned up, kept {keep_suite} suite ({len(keep_videos)} videos)")
else:
# Remove all
shutil.rmtree(self.base_dir, ignore_errors=True)
print("✓ Removed all test videos")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Manage test video suite")
parser.add_argument("--setup", action="store_true", help="Set up complete suite")
parser.add_argument("--validate", action="store_true", help="Validate existing suite")
parser.add_argument("--cleanup", action="store_true", help="Clean up test videos")
parser.add_argument("--keep", help="Keep specific suite when cleaning")
parser.add_argument("--base-dir", default="tests/fixtures/videos",
help="Base directory for test videos")
args = parser.parse_args()
manager = TestSuiteManager(Path(args.base_dir))
if args.setup:
manager.setup()
elif args.validate:
manager.validate()
manager.generate_config()
elif args.cleanup:
manager.cleanup(keep_suite=args.keep)
else:
parser.print_help()