Coverage for tests/streamwise_app/test_streamanimate.py: 94%
124 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 04:47 +0000
1#!/usr/bin/env python3
2"""
3Unit tests for StreamAnimate.
4"""
6import os
7import sys
8import pytest
10from http import HTTPStatus
12from PIL import Image # noqa: F401 - import before patch.dict to keep PIL in sys.modules
14from quart import Quart
16from unittest.mock import patch
17from unittest.mock import MagicMock
19# Add current path
20sys.path.append(os.getcwd())
22from tests.test_utils import temp_sys_path
23from tests.torch_mock import TorchMock
24from tests.streamwise_app.app_test_helpers import check_app_root
25from tests.streamwise_app.app_test_helpers import check_health
26from tests.streamwise_app.app_test_helpers import check_files
27from tests.streamwise_app.app_test_helpers import check_unknown_route
28from tests.streamwise_app.app_test_helpers import check_job_submit_page
29from tests.streamwise_app.app_test_helpers import check_job_status_page
30from tests.streamwise_app.app_test_helpers import check_api_job_status
31from tests.streamwise_app.app_test_helpers import check_api_job_requests
33mock_torch = TorchMock()
35mock_modules = {}
36mock_modules.update(mock_torch.get_sub_modules())
38with patch.dict(sys.modules, mock_modules):
39 with temp_sys_path("apps", "apps/streamanimate"):
40 from apps.streamanimate.streamanimate import StreamAnimateApp
41 from apps.streamanimate.streamanimate_job import StreamAnimateJob
42 from apps.streamanimate.streamanimate_job import JobStatus
43 from apps.streamanimate.animate_prompts import IMG_PROMPT
44 from apps.streamanimate.animate_prompts import VIDEO_PROMPT
45 from tests.streamwise_app.lmm_generator_mock import LMMGeneratorMock
48streamanimate_app = StreamAnimateApp()
51@pytest.fixture(name="test_app")
52def _test_app() -> Quart:
53 return streamanimate_app.app
56@pytest.mark.asyncio
57async def test_app(test_app: Quart) -> None:
58 """Check that GET / returns 200."""
59 await check_app_root(test_app, "StreamAnimate")
62@pytest.mark.asyncio
63async def test_health(test_app: Quart) -> None:
64 """Check /health."""
65 await check_health(test_app)
68@pytest.mark.asyncio
69async def test_files(test_app: Quart) -> None:
70 """Check /files endpoint."""
71 await check_files(test_app, "streamanimate")
74@pytest.mark.asyncio
75async def test_unknown_route(test_app: Quart) -> None:
76 """Check that an unknown route returns 404."""
77 await check_unknown_route(test_app)
80@pytest.mark.asyncio
81async def test_job_submit_page(test_app: Quart) -> None:
82 """Check the web page for job submission."""
83 await check_job_submit_page(test_app)
86@pytest.mark.asyncio
87async def test_job_status_page(test_app: Quart) -> None:
88 """Check the web page for job status."""
89 await check_job_status_page(test_app)
92@pytest.mark.asyncio
93async def test_api_job_status(test_app: Quart) -> None:
94 """Check the API for job status (returns UNKNOWN for nonexistent jobs)."""
95 await check_api_job_status(test_app)
98@pytest.mark.asyncio
99async def test_api_job_requests(test_app: Quart) -> None:
100 """Check the API for job requests listing (returns empty for nonexistent jobs)."""
101 await check_api_job_requests(test_app)
104@pytest.mark.asyncio
105async def test_submit_job(test_app: Quart) -> None:
106 """Check the API for job requests."""
107 client = test_app.test_client()
109 response = await client.post("/api/job", json={"text_prompt": "A bird flying"})
110 assert response.status_code == HTTPStatus.BAD_REQUEST
111 response_json = await response.get_json()
112 assert "error" in response_json
113 assert response_json["error"] == "Service manager not initialized"
115 # Mock the service manager
116 streamanimate_app.service_manager = MagicMock()
117 streamanimate_app.service_manager.get_service_url = MagicMock(
118 return_value="http://mock_service_url:1234"
119 )
121 response = await client.post("/api/job", json={"text_prompt": "A bird flying over mountains"})
122 # The job is accepted; it may fail later if external services are unavailable.
123 response_json = await response.get_json()
124 assert response_json is not None
125 assert "status" in response_json
126 # If the image generation service responds before 0.1s we may get an error;
127 # if the job is still running after 0.1s we get job_id back.
128 if response.status_code == HTTPStatus.OK:
129 assert "job_id" in response_json
130 assert response_json["status"] == "success"
131 else:
132 assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
133 assert "error" in response_json
136def test_animate_prompts() -> None:
137 """Test that animate prompts are defined and non-empty."""
138 assert IMG_PROMPT
139 assert len(IMG_PROMPT) > 0
140 assert VIDEO_PROMPT
141 assert "animation" in VIDEO_PROMPT.lower() or "motion" in VIDEO_PROMPT.lower()
144@pytest.mark.asyncio
145async def test_gen_animate_no_video_no_audio() -> None:
146 """StreamAnimateJob.gen_animate raises ValueError when no video/audio generated."""
147 service_manager = MagicMock()
148 job = StreamAnimateJob(
149 job_id="test_gen_animate_no_video_no_audio",
150 service_manager=service_manager,
151 )
152 job.gen = LMMGeneratorMock()
153 job.config["output_mode"] = "audio_only"
154 # narration_text is empty and audio_only => neither video nor audio generated
155 with pytest.raises(ValueError, match="Neither video nor audio was generated"):
156 await job.gen_animate(
157 image_base64=None,
158 text_prompt="A bird flying",
159 narration_text="",
160 )
161 job_status = await job.get_status()
162 assert job_status == JobStatus.FAILED
164 del job
165 del service_manager
168@pytest.mark.asyncio
169async def test_gen_animate_text_only() -> None:
170 """StreamAnimateJob.gen_animate with text prompt generates video or fails gracefully."""
171 service_manager = MagicMock()
172 job = StreamAnimateJob(
173 job_id="test_gen_animate_text_only",
174 service_manager=service_manager,
175 )
176 job.gen = LMMGeneratorMock()
177 job.config["output_mode"] = "video_audio_unsynced"
179 try:
180 await job.gen_animate(
181 image_base64=None,
182 text_prompt="A bird flying over mountains",
183 narration_text="",
184 )
185 job_status = await job.get_status()
186 assert job_status == JobStatus.COMPLETED
187 except FileNotFoundError:
188 # ffmpeg not available in this environment
189 job_status = await job.get_status()
190 assert job_status == JobStatus.FAILED
192 del job
193 del service_manager
196@pytest.mark.asyncio
197async def test_gen_animate_with_narration() -> None:
198 """StreamAnimateJob.gen_animate with narration generates video and audio or fails gracefully."""
199 service_manager = MagicMock()
200 job = StreamAnimateJob(
201 job_id="test_gen_animate_with_narration",
202 service_manager=service_manager,
203 )
204 job.gen = LMMGeneratorMock()
205 job.config["output_mode"] = "video_audio_unsynced"
207 try:
208 await job.gen_animate(
209 image_base64=None,
210 text_prompt="A sunset over the ocean",
211 narration_text="Watch the beautiful sunset over the ocean.",
212 )
213 job_status = await job.get_status()
214 assert job_status == JobStatus.COMPLETED
215 except FileNotFoundError:
216 # ffmpeg not available in this environment
217 job_status = await job.get_status()
218 assert job_status == JobStatus.FAILED
220 del job
221 del service_manager