245 lines
8.1 KiB
Python
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"
|