Coverage for tests/test_run_httpserver_mock.py: 99%

255 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 04:47 +0000

1#!/usr/bin/env python3 

2 

3import sys 

4import pytest 

5import asyncio 

6import logging 

7 

8from pytest import raises 

9 

10from http import HTTPStatus 

11 

12from unittest.mock import patch 

13from unittest.mock import MagicMock 

14from unittest.mock import AsyncMock 

15from unittest.mock import ANY 

16 

17from tests.torch_mock import TorchMock 

18 

19from streamwise_apps import STREAMWISE_APPS 

20from streamwise_apps import VLLM_SERVICES 

21 

22mock_torch = TorchMock() 

23 

24sys.path.append("wrapper") 

25 

26# Quart mocks 

27mock_quart = MagicMock() 

28mock_quart.route = lambda *args, **kwargs: (lambda f: f) 

29mock_quart.render_template = AsyncMock(return_value="mocked_template") 

30mock_quart.send_file = AsyncMock(return_value="mocked_file") 

31mock_quart.send_from_directory = AsyncMock(return_value="mocked_file") 

32mock_quart.jsonify = lambda x: x 

33mock_app = MagicMock() 

34mock_quart.Quart = MagicMock(return_value=mock_app) 

35mock_quart.Response = lambda *args, **kwargs: {"response": args, "kwargs": kwargs} 

36mock_request = MagicMock() 

37mock_request.args = {} 

38mock_request.json = {} 

39mock_request.get_json = AsyncMock(return_value={}) 

40mock_quart.request = mock_request 

41 

42mock_serve = AsyncMock() 

43 

44mock_modules = { 

45 'nvidia_smi': MagicMock(), 

46 'torch': mock_torch, 

47 'hypercorn': MagicMock(), 

48 'hypercorn.config': MagicMock(), 

49 'hypercorn.asyncio': MagicMock(serve=mock_serve), 

50 'quart': mock_quart, 

51 "quart.request": mock_quart.request, 

52 "quart.jsonify": mock_quart.jsonify, 

53 "quart.send_file": mock_quart.send_file, 

54 "quart.send_from_directory": mock_quart.send_from_directory, 

55 "quart.render_template": mock_quart.render_template, 

56 "quart.route": mock_quart.route, 

57 "quart.Response": mock_quart.Response, 

58 'imageio': MagicMock(), 

59 'cv2': MagicMock(), 

60 'xfuser': MagicMock(), 

61 'sklearn': MagicMock(), 

62 'scipy': MagicMock(), 

63 'scipy.stats': MagicMock(), 

64} 

65mock_modules.update(mock_torch.get_sub_modules()) 

66 

67with patch.dict(sys.modules, mock_modules): 

68 from run_httpserver import main 

69 from run_httpserver import arg_parsing 

70 from run_httpserver import send_task 

71 from run_httpserver import nccl_worker 

72 from run_httpserver import wait_for_everybody 

73 from run_httpserver import index 

74 from run_httpserver import gen_img 

75 from run_httpserver import gen_video 

76 from run_httpserver import gen_audio 

77 from run_httpserver import get_service_names 

78 from run_httpserver import setup_dist_environment 

79 import run_httpserver as _run_httpserver 

80 

81 

82def test_get_service_names() -> None: 

83 """Check that we have the expected services covered.""" 

84 service_names = get_service_names() 

85 assert len(service_names) == 48 

86 assert "mock" in service_names 

87 assert "wan" in service_names 

88 assert "wan22" in service_names 

89 assert "fantasytalking" in service_names 

90 assert "flux" in service_names 

91 assert "fluxkrea" in service_names 

92 assert "qwenimage" in service_names 

93 assert "qwenimageedit" in service_names 

94 assert "hunyuanimage" in service_names 

95 assert "yolo" in service_names 

96 assert "kokoro" in service_names 

97 assert "vibevoice" in service_names 

98 assert "notexisting" not in service_names 

99 

100 for vllm_service in VLLM_SERVICES: 

101 if vllm_service != "llm": 

102 assert vllm_service in service_names 

103 

104 # Applications 

105 for streamwise_app in STREAMWISE_APPS: 

106 assert streamwise_app in service_names 

107 

108 

109@pytest.mark.asyncio 

110async def test_server_help() -> None: 

111 """Check the HTTP server start.""" 

112 test_args = ["run_http_server.py", "--help"] 

113 with patch.object(sys, "argv", test_args): 

114 with raises(SystemExit) as exc_info: 

115 await main() 

116 assert exc_info.value.code == 0 

117 

118 

119# This test is expensive; we swallow the timeout 

120# @pytest.mark.timeout(2) 

121@pytest.mark.asyncio 

122async def test_server() -> None: 

123 """Check the base HTTP server start.""" 

124 test_args = ["run_http_server.py"] 

125 with patch.object(sys, "argv", test_args): 

126 try: 

127 # Use asyncio.wait_for to limit the runtime to 2s 

128 await asyncio.wait_for(main(), timeout=2) 

129 except (asyncio.TimeoutError, asyncio.CancelledError): 

130 print("HTTP server finished (swallowed)") 

131 

132 

133@pytest.mark.asyncio 

134async def test_server_mock() -> None: 

135 """Check the HTTP server start for the mock service.""" 

136 test_args = ["run_http_server.py", "--mock"] 

137 with patch.object(sys, "argv", test_args): 

138 await main() 

139 

140 

141@pytest.mark.asyncio 

142async def test_server_imageresize() -> None: 

143 """Check the HTTP server start for the image resize service.""" 

144 test_args = ["run_http_server.py", "--imageresize"] 

145 with patch.object(sys, "argv", test_args): 

146 await main() 

147 

148 

149@pytest.mark.asyncio 

150@pytest.mark.parametrize("model_name", [ 

151 "flux", 

152 "hidream", 

153 "fantasytalking", 

154 "hunyuanframepack", 

155 "hunyuanframepackf1", 

156 "wan", 

157 "wan21", 

158 "wan22", 

159]) 

160async def test_server_service_diffusers(model_name: str) -> None: 

161 """Check the HTTP server start for multiple services. 

162 These services fail with: 

163 RuntimeError: Failed to import diffusers.pipelines.pipeline_utils 

164 """ 

165 test_args = ["run_http_server.py", f"--{model_name}"] 

166 with patch.object(sys, "argv", test_args): 

167 with raises( 

168 (ModuleNotFoundError, RuntimeError), 

169 match="(diffusers|This module requires CUDA support|Found no NVIDIA driver)" 

170 ): 

171 await main() 

172 

173 

174@pytest.mark.asyncio 

175@pytest.mark.parametrize("model_name", [ 

176 "realesrgan", 

177 "yolo", # ultralytics 

178 "kokoro", # KPipeline 

179 "xtts", # TTS 

180 # "vibevoice", # demo 

181 "podcasttranscript", # fitz 

182 # "januspro" # janus 

183]) 

184async def test_server_service_import(model_name: str) -> None: 

185 """Check the HTTP server start for multiple services. 

186 They fail with the following errors: 

187 ModuleNotFoundError: No module named 'RealESRGAN' 

188 ModuleNotFoundError: No module named 'fitz' 

189 ModuleNotFoundError: No module named 'janus' 

190 ImportError: cannot import name 'KPipeline' from 'kokoro' 

191 """ 

192 test_args = ["run_http_server.py", f"--{model_name}"] 

193 with patch.object(sys, "argv", test_args): 

194 with pytest.raises((ModuleNotFoundError, ImportError)): 

195 await main() 

196 

197 

198@pytest.mark.asyncio 

199async def test_server_all_services() -> None: 

200 """Check the HTTP server start for all services.""" 

201 service_names = get_service_names() 

202 for vllm_service in VLLM_SERVICES: 

203 if vllm_service in service_names: 

204 service_names.remove(vllm_service) # Separate vLLM service 

205 for streamwise_app in STREAMWISE_APPS: 

206 if streamwise_app in service_names: 

207 service_names.remove(streamwise_app) # Separate service 

208 service_names.remove("streamwise") # Separate service 

209 

210 for model_name in service_names: 

211 try: 

212 test_args = ["run_http_server.py", f"--{model_name}"] 

213 with patch.object(sys, "argv", test_args): 

214 try: 

215 logging.info(f"Testing service: {model_name}") 

216 # Use asyncio.wait_for to limit the runtime to 2s 

217 await asyncio.wait_for(main(), timeout=2.0) 

218 except (asyncio.TimeoutError, asyncio.CancelledError): 

219 logging.error(f"HTTP server for {model_name} finished (swallowed)") 

220 except ( 

221 ModuleNotFoundError, 

222 ImportError, 

223 RuntimeError, 

224 TypeError, # Janus Pro 

225 ): 

226 logging.warning(f"Skipping service {model_name} due to error") 

227 finally: 

228 loop = asyncio.get_running_loop() 

229 executor = getattr(loop, "_default_executor", None) 

230 if executor: 

231 executor.shutdown(wait=False, cancel_futures=True) 

232 

233 

234@pytest.mark.asyncio 

235async def test_server_wrong_server() -> None: 

236 """Check the HTTP server start with a wrong argument.""" 

237 with raises(SystemExit) as exc_info: 

238 test_args = ["run_http_server.py", "--wrong"] 

239 with patch.object(sys, "argv", test_args): 

240 await main() 

241 assert exc_info.value.code == 2 

242 

243 

244def test_arg_parsing() -> None: 

245 """Check the argument parsing.""" 

246 test_args = ["run_http_server.py", "--wan", "--flux", "--hunyuanframepackf1"] 

247 with patch.object(sys, "argv", test_args): 

248 args, engine_config = arg_parsing() 

249 assert args.wan is True 

250 assert args.flux is True 

251 assert args.fluxupscaler is False 

252 assert args.fluxkontext is False 

253 assert args.fluxkrea is False 

254 assert args.hidream is False 

255 assert args.qwenimage is False 

256 assert args.qwenimageedit is False 

257 assert args.realesrgan is False 

258 assert args.kokoro is False 

259 assert args.hunyuanframepack is False 

260 assert args.hunyuanframepackf1 is True 

261 assert args.hunyuanframepackvae is False 

262 assert engine_config is None 

263 

264 test_args = ["run_http_server.py", "--nonexisting"] 

265 with patch.object(sys, "argv", test_args): 

266 with raises(SystemExit) as exc_info: 

267 args, engine_config = arg_parsing() 

268 assert exc_info.value.code == 2 

269 

270 

271@pytest.mark.asyncio 

272async def test_send_task() -> None: 

273 """Basic call to cover the function.""" 

274 await send_task({}) 

275 

276 

277@pytest.mark.asyncio 

278async def test_nccl_worker() -> None: 

279 """Basic call to cover the function.""" 

280 await nccl_worker() 

281 

282 

283@pytest.mark.asyncio 

284async def test_wait_for_everybody() -> None: 

285 """Basic call to cover the function.""" 

286 await wait_for_everybody() 

287 

288 

289@pytest.mark.asyncio 

290async def test_content() -> None: 

291 """Check the HTTP server content.""" 

292 with raises(TypeError): 

293 # TODO fix: TypeError: object MagicMock can't be used in 'await' expression 

294 assert await index() == "index page" 

295 # assert await health() == {} 

296 # assert await model_health("dummy") == {"ok": True} 

297 

298 

299@pytest.mark.asyncio 

300async def test_gen_img() -> None: 

301 """Check the image generation endpoint.""" 

302 response = await gen_img(None) 

303 assert response == ({"error": "Not initialized"}, HTTPStatus.INTERNAL_SERVER_ERROR) 

304 

305 mock_model = AsyncMock() 

306 mock_model.status = "ok" 

307 mock_model.running = True 

308 response = await gen_img(mock_model) 

309 assert response == ({"error": "Generation in progress"}, HTTPStatus.SERVICE_UNAVAILABLE) 

310 

311 mock_model.running = False 

312 response = await gen_img(mock_model) 

313 assert response is not None 

314 # assert response == "mocked_file" 

315 

316 

317@pytest.mark.asyncio 

318async def test_gen_video() -> None: 

319 """Check the video generation endpoint.""" 

320 response = await gen_video(None) 

321 assert response == ({"error": "Not initialized"}, HTTPStatus.INTERNAL_SERVER_ERROR) 

322 

323 mock_model = MagicMock() 

324 mock_model.status = "ok" 

325 mock_model.generate = AsyncMock(return_value="mocked_file.mp4") 

326 mock_model.get_rest_args = AsyncMock(return_value={ 

327 "task": "mock", 

328 "args": {} 

329 }) 

330 mock_model.running = True 

331 response = await gen_video(mock_model) 

332 assert response == ({"error": "Generation in progress"}, HTTPStatus.SERVICE_UNAVAILABLE) 

333 

334 mock_model.running = False 

335 response = await gen_video(mock_model) 

336 assert response is not None 

337 response_msg, code = response 

338 assert code == HTTPStatus.INTERNAL_SERVER_ERROR 

339 assert response_msg["error"] == "Video file not found: mocked_file.mp4" 

340 

341 

342@pytest.mark.asyncio 

343async def test_gen_audio() -> None: 

344 """Check the audio generation endpoint.""" 

345 response = await gen_audio(None) 

346 assert response == ({"error": "Not initialized"}, HTTPStatus.INTERNAL_SERVER_ERROR) 

347 

348 # Blocked generation 

349 mock_model = MagicMock() 

350 mock_model.status = "ok" 

351 mock_model.generate = AsyncMock(return_value="mocked_file.wav") 

352 mock_model.get_rest_args = AsyncMock(return_value={ 

353 "task": "mock", 

354 "args": {} 

355 }) 

356 mock_model.running = True 

357 response = await gen_audio(mock_model) 

358 assert response == ({"error": "Generation in progress"}, HTTPStatus.SERVICE_UNAVAILABLE) 

359 

360 # Missing file 

361 mock_model.running = False 

362 response = await gen_audio(mock_model) 

363 assert response is not None 

364 response_msg, code = response 

365 assert code == HTTPStatus.INTERNAL_SERVER_ERROR 

366 assert response_msg["error"] == "Audio file not found: mocked_file.wav" 

367 

368 # No output 

369 mock_model.generate = AsyncMock(return_value=None) 

370 response = await gen_audio(mock_model) 

371 assert response is not None 

372 response_msg, code = response 

373 assert code == HTTPStatus.INTERNAL_SERVER_ERROR 

374 assert response_msg["error"] == "No audio generated" 

375 

376 # Use existing audio file 

377 mock_quart.send_from_directory.reset_mock() 

378 mock_model.generate = AsyncMock(return_value="tests/data/audio_4675.wav") 

379 response = await gen_audio(mock_model) 

380 assert response is not None 

381 assert response == "mocked_file" # send_from_directory() returns this 

382 mock_quart.send_from_directory.assert_awaited_once_with( 

383 "tests/data", 

384 "audio_4675.wav", 

385 mimetype="audio/wav", 

386 as_attachment=True, 

387 attachment_filename=ANY, 

388 ) 

389 mock_quart.send_file.assert_not_awaited() 

390 

391 

392def test_setup_dist_environment_mig_warning(caplog: pytest.LogCaptureFixture) -> None: 

393 """ 

394 When world_size > visible CUDA devices (MIG partition case), setup_dist_environment 

395 must log a warning and clamp world_size to the number of visible devices. 

396 The TorchMock returns device_count=1, so setting WORLD_SIZE=2 triggers the path. 

397 """ 

398 import os 

399 

400 saved_env = { 

401 k: os.environ.get(k) 

402 for k in ("MASTER_ADDR", "MASTER_PORT", "RANK", "LOCAL_RANK", 

403 "NODE_RANK", "WORLD_SIZE", "LOCAL_WORLD_SIZE", "NPROC_PER_NODE") 

404 } 

405 try: 

406 os.environ["MASTER_ADDR"] = "localhost" 

407 os.environ["MASTER_PORT"] = "12355" 

408 os.environ["RANK"] = "1" 

409 os.environ["LOCAL_RANK"] = "1" 

410 os.environ["NODE_RANK"] = "0" 

411 os.environ["WORLD_SIZE"] = "2" # 2 processes, but only 1 visible device 

412 os.environ["LOCAL_WORLD_SIZE"] = "2" 

413 

414 with caplog.at_level(logging.WARNING): 

415 setup_dist_environment() 

416 

417 # world_size must be clamped down to the number of visible CUDA devices (1) 

418 assert _run_httpserver.world_size == 1 

419 assert _run_httpserver.local_world_size == 1 

420 # And a MIG-related warning must have been logged 

421 assert any( 

422 "world_size=2" in record.message and "MIG" in record.message 

423 for record in caplog.records 

424 ), f"Expected MIG warning, got: {[r.message for r in caplog.records]}" 

425 finally: 

426 # Restore original env and module globals 

427 for k, v in saved_env.items(): 

428 if v is None: 

429 os.environ.pop(k, None) 

430 else: 

431 os.environ[k] = v 

432 _run_httpserver.rank = 0 

433 _run_httpserver.world_size = 1 

434 _run_httpserver.local_world_size = 1