Files
video-gen/tests/unit/test_cli.py
2026-02-04 08:52:10 -05:00

245 lines
8.1 KiB
Python

"""
Tests for CLI module.
"""
import pytest
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
from typer.testing import CliRunner
from src.cli.main import (
app,
align_resolution,
align_num_frames,
cap_resolution_for_backend,
cap_num_frames_for_backend,
format_duration,
)
runner = CliRunner()
# Valid storyboard JSON template
VALID_STORYBOARD = """
{
"schema_version": "1.0",
"project": {
"title": "Test Storyboard",
"fps": 24,
"target_duration_s": 20,
"resolution": {"width": 1280, "height": 720},
"aspect_ratio": "16:9"
},
"shots": [
{
"id": "shot_001",
"duration_s": 4,
"prompt": "A test shot description",
"camera": {"framing": "wide", "movement": "static"},
"generation": {"seed": 42, "steps": 30, "cfg_scale": 6.0}
}
],
"output": {
"container": "mp4",
"codec": "h264",
"crf": 18,
"preset": "medium"
}
}
"""
class TestValidateCommand:
"""Test the validate command."""
def test_validate_nonexistent_file(self):
"""Test validation with non-existent file."""
result = runner.invoke(app, ["validate", "nonexistent.json"])
assert result.exit_code == 1
assert "not found" in result.output
def test_validate_valid_storyboard(self):
"""Test validation with valid storyboard."""
with tempfile.TemporaryDirectory() as tmpdir:
storyboard_file = Path(tmpdir) / "test.json"
storyboard_file.write_text(VALID_STORYBOARD)
result = runner.invoke(app, ["validate", str(storyboard_file)])
assert result.exit_code == 0
assert "valid" in result.output
assert "Test Storyboard" in result.output
def test_validate_verbose(self):
"""Test validation with verbose flag."""
with tempfile.TemporaryDirectory() as tmpdir:
storyboard_file = Path(tmpdir) / "test.json"
storyboard_file.write_text(VALID_STORYBOARD)
result = runner.invoke(app, ["validate", str(storyboard_file), "--verbose"])
assert result.exit_code == 0
assert "Shots" in result.output
class TestListBackendsCommand:
"""Test the list-backends command."""
def test_list_backends(self):
"""Test listing available backends."""
result = runner.invoke(app, ["list-backends"])
assert result.exit_code == 0
assert "wan_t2v_14b" in result.output
assert "Available Backends" in result.output
class TestGenerateCommand:
"""Test the generate command."""
@pytest.fixture
def mock_storyboard(self):
"""Create a mock storyboard file."""
with tempfile.TemporaryDirectory() as tmpdir:
storyboard_file = Path(tmpdir) / "test.json"
storyboard_file.write_text(VALID_STORYBOARD)
yield storyboard_file
def test_generate_dry_run(self, mock_storyboard):
"""Test generate command with dry-run flag."""
result = runner.invoke(app, [
"generate",
str(mock_storyboard),
"--dry-run"
])
assert result.exit_code == 0
assert "Dry run complete" in result.output
assert "Elapsed:" in result.output
def test_generate_nonexistent_storyboard(self):
"""Test generate with non-existent storyboard."""
result = runner.invoke(app, ["generate", "nonexistent.json"])
assert result.exit_code == 1
assert "not found" in result.output
class TestResumeCommand:
"""Test the resume command."""
def test_resume_nonexistent_project(self):
"""Test resume with non-existent project."""
with tempfile.TemporaryDirectory() as tmpdir:
result = runner.invoke(app, [
"resume",
"nonexistent_project",
"--output", tmpdir
])
assert result.exit_code == 1
assert "No checkpoint found" in result.output
def test_resume_existing_project(self):
"""Test resume with existing project checkpoint."""
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) / "test_project"
project_dir.mkdir()
checkpoint_db = project_dir / "checkpoints.db"
checkpoint_db.touch()
# Create a mock checkpoint
with patch('src.cli.main.CheckpointManager') as mock_mgr:
mock_checkpoint = MagicMock()
mock_checkpoint.storyboard_path = str(Path(tmpdir) / "storyboard.json")
mock_mgr.return_value.get_project.return_value = mock_checkpoint
# Create storyboard file
storyboard_file = Path(tmpdir) / "storyboard.json"
storyboard_file.write_text(VALID_STORYBOARD)
result = runner.invoke(app, [
"resume",
"test_project",
"--output", tmpdir
])
# Should attempt to resume (may fail on actual generation)
assert "Resuming project" in result.output or result.exit_code != 0
class TestCLIHelp:
"""Test CLI help messages."""
def test_main_help(self):
"""Test main help message."""
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Storyboard to Video Generation Pipeline" in result.output
def test_generate_help(self):
"""Test generate command help."""
result = runner.invoke(app, ["generate", "--help"])
assert result.exit_code == 0
assert "Generate video from storyboard" in result.output
assert "--output" in result.output
assert "--backend" in result.output
assert "--dry-run" in result.output
def test_validate_help(self):
"""Test validate command help."""
result = runner.invoke(app, ["validate", "--help"])
assert result.exit_code == 0
assert "Validate a storyboard file" in result.output
def test_resume_help(self):
"""Test resume command help."""
result = runner.invoke(app, ["resume", "--help"])
assert result.exit_code == 0
assert "Resume a failed or interrupted project" in result.output
class TestCLIAlignmentHelpers:
"""Test CLI alignment helpers."""
def test_align_resolution_no_change(self):
"""Resolution already aligned to 16."""
width, height = align_resolution(1920, 1088)
assert width == 1920
assert height == 1088
def test_align_resolution_adjusts(self):
"""Resolution is rounded up to multiple of 16."""
width, height = align_resolution(1920, 1080)
assert width == 1920
assert height == 1088
def test_align_num_frames_no_change(self):
"""Frame count already valid."""
assert align_num_frames(97) == 97 # 97 - 1 is divisible by 4
def test_align_num_frames_adjusts(self):
"""Frame count is rounded up to valid value."""
assert align_num_frames(96) == 97
def test_cap_resolution_for_small_backend(self):
"""Smaller backend caps resolution."""
width, height = cap_resolution_for_backend("wan_t2v_1_3b", 1920, 1080)
assert width == 1280
assert height == 720
def test_cap_resolution_for_other_backends(self):
"""No cap for other backends."""
width, height = cap_resolution_for_backend("wan_t2v_14b", 1920, 1080)
assert width == 1920
assert height == 1080
def test_cap_num_frames_for_small_backend(self):
"""Smaller backend caps frame count."""
assert cap_num_frames_for_backend("wan_t2v_1_3b", 121) == 97
def test_cap_num_frames_for_other_backends(self):
"""No cap for other backends."""
assert cap_num_frames_for_backend("wan_t2v_14b", 121) == 121
def test_format_duration(self):
"""Format duration for display."""
assert format_duration(5) == "0m05s"
assert format_duration(65) == "1m05s"