Coverage for tests/test_run_httpserver.py: 100%

229 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 os 

6import tempfile 

7 

8from http import HTTPStatus 

9 

10from unittest.mock import patch 

11from unittest.mock import MagicMock 

12from unittest.mock import AsyncMock 

13 

14from tests.test_utils import temp_sys_path 

15from tests.torch_mock import TorchMock 

16 

17TMP_DIR = "tmp" 

18 

19mock_torch = TorchMock() 

20 

21 

22with temp_sys_path("wrapper"): 

23 from tests.test_wrapper_model import MockModelGeneration 

24 

25with patch.dict(sys.modules, { 

26 'nvidia_smi': MagicMock(), 

27 'torch': mock_torch, 

28 'torch.distributed': MagicMock(), 

29 'imageio': MagicMock(), 

30 'cv2': MagicMock(), 

31 'xfuser': MagicMock(), 

32}): 

33 with temp_sys_path("wrapper"): 

34 import run_httpserver 

35 

36 

37@pytest.fixture(autouse=True) 

38def clear_models() -> None: 

39 run_httpserver.models = {} 

40 

41 

42@pytest.mark.asyncio 

43async def test_files() -> None: 

44 """Check the files endpoints.""" 

45 app = run_httpserver.app 

46 client = app.test_client() 

47 response = await client.get("/files") 

48 assert response is not None 

49 response_json = await response.json 

50 assert isinstance(response_json, dict) 

51 

52 response = await client.get("/file/filename.txt") 

53 assert response is not None 

54 assert response.status_code == HTTPStatus.NOT_FOUND 

55 response_json = await response.json 

56 assert isinstance(response_json, dict) 

57 assert "error" in response_json 

58 assert response_json["error"] == "File not found" 

59 

60 # Create temp file in /tmp 

61 with tempfile.NamedTemporaryFile(mode="w", delete=False, dir="/tmp", suffix=".txt") as tmp_file: 

62 tmp_file.write("Test file content") 

63 tmp_filename = tmp_file.name 

64 filename = os.path.basename(tmp_filename) 

65 

66 # Download 

67 response = await client.get(f"/file/{filename}") 

68 assert response is not None 

69 assert response.status_code == HTTPStatus.OK 

70 content = await response.get_data(as_text=True) 

71 assert content == "Test file content" 

72 

73 # Info 

74 response = await client.get(f"/file_info/{filename}") 

75 assert response is not None 

76 assert response.status_code == HTTPStatus.OK 

77 response_json = await response.json 

78 assert len(response_json) > 0 

79 assert response_json["size"] == 17 

80 assert response_json["mimetype"] == "text/plain" 

81 assert response_json["name"].endswith(".txt") 

82 assert response_json["date"] > 0 

83 assert response_json["type"] == "text" 

84 # get_text_file_info() 

85 assert response_json["num_chars"] == 17 

86 assert response_json["num_words"] == 3 

87 assert response_json["num_lines"] == 1 

88 

89 os.unlink(tmp_filename) 

90 

91 

92@pytest.mark.timeout(5) 

93@pytest.mark.asyncio 

94async def test_yolo() -> None: 

95 """Check the YOLO endpoint.""" 

96 app = run_httpserver.app 

97 client = app.test_client() 

98 response = await client.post('/yolo') 

99 assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 

100 

101 

102@pytest.mark.timeout(5) 

103@pytest.mark.asyncio 

104async def test_transcript() -> None: 

105 """Check the transcript endpoint.""" 

106 app = run_httpserver.app 

107 client = app.test_client() 

108 response = await client.post("/podcasttranscript") 

109 assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 

110 response_json = await response.json 

111 assert isinstance(response_json, dict) 

112 assert "error" in response_json 

113 assert response_json["error"] == "Podcast transcript model not initialized" 

114 

115 run_httpserver.models = {"podcasttranscript": MockModelGeneration()} 

116 response = await client.post("/podcasttranscript") 

117 assert response.status_code == HTTPStatus.BAD_REQUEST 

118 response_json = await response.json 

119 assert isinstance(response_json, dict) 

120 assert "error" in response_json 

121 assert response_json["error"] == "No JSON body received" 

122 

123 # Setting up the mock model 

124 run_httpserver.models = {"podcasttranscript": MockModelGeneration()} 

125 

126 # Transcript mocked 

127 response = await client.post( 

128 "/podcasttranscript", 

129 json={"args": {}}) 

130 assert response.status_code == HTTPStatus.BAD_REQUEST 

131 response_json = await response.json 

132 assert isinstance(response_json, dict) 

133 assert "error" in response_json 

134 assert response_json["error"] == "Model not initialized. Current status: initializing." 

135 

136 # Transcript stream 

137 response = await client.post("/podcasttranscript/stream") 

138 assert response.status_code == HTTPStatus.BAD_REQUEST 

139 

140 with pytest.raises(AssertionError): 

141 response = await client.post( 

142 "/podcasttranscript/stream", 

143 json={ 

144 "args": { 

145 "prompt": "Test prompt" 

146 } 

147 }) 

148 assert response.status_code == HTTPStatus.BAD_REQUEST 

149 

150 

151@pytest.mark.asyncio 

152async def test_kokoro() -> None: 

153 """Check the kokoro endpoint.""" 

154 app = run_httpserver.app 

155 client = app.test_client() 

156 response = await client.post("/kokoro") 

157 assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 

158 

159 

160@pytest.mark.asyncio 

161async def test_service_endpoints() -> None: 

162 app = run_httpserver.app 

163 client = app.test_client() 

164 service_names = [ 

165 "flux", "fluxkontext", "fluxkrea", "fluxupscaler", "fluxupscaler/video", 

166 "fantasytalking", "hunyuanavatar", 

167 "kokoro", "xtts", "vibevoice", 

168 "hidream", 

169 "realesrgan", "realesrgan/video", 

170 "qwenimage", "qwenimageedit", 

171 "realesrgan", "imageresize", 

172 "bagel", "llamagen", "januspro", 

173 "thinksound", "dia", 

174 "ltx", "wan", "wan22", 

175 "hunyuanframepack", "hunyuanframepackf1", 

176 ] 

177 for service_name in service_names: 

178 response = await client.post(f"/{service_name}") 

179 status = response.status_code 

180 assert status == HTTPStatus.INTERNAL_SERVER_ERROR, f"Service {service_name} failed with {status}" 

181 

182 

183@pytest.mark.asyncio 

184async def test_service_health() -> None: 

185 """Check the service health endpoint.""" 

186 app = run_httpserver.app 

187 client = app.test_client() 

188 response = await client.get("/wan/health") 

189 assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR 

190 

191 

192@pytest.mark.asyncio 

193async def test_mock_model() -> None: 

194 """Check the service health endpoint.""" 

195 app = run_httpserver.app 

196 client = app.test_client() 

197 

198 # Setup mock model 

199 run_httpserver.models = {"mockmodel": MockModelGeneration()} 

200 

201 # Health 

202 response = await client.get("/mockmodel/health") 

203 assert response.status_code == HTTPStatus.OK 

204 model_health = await response.json 

205 assert len(model_health) > 0 

206 assert "gen_timer" in model_health 

207 

208 

209@pytest.mark.asyncio 

210async def test_yolo_mock() -> None: 

211 """Check YOLO using mocks.""" 

212 app = run_httpserver.app 

213 client = app.test_client() 

214 

215 # Setup mock model for YOLO 

216 run_httpserver.models = { 

217 "yolo": MockModelGeneration(output_type="list_pillow") 

218 } 

219 

220 # Health 

221 response = await client.get("/yolo/health") 

222 assert response.status_code == HTTPStatus.OK 

223 model_health = await response.json 

224 assert len(model_health) > 0 

225 assert "gen_timer" in model_health 

226 

227 # Generate with no body 

228 response = await client.post("/yolo") 

229 assert response.status_code == HTTPStatus.BAD_REQUEST 

230 response_json = await response.json 

231 assert "error" in response_json 

232 error_msg = response_json["error"] 

233 assert "No JSON body received" in error_msg 

234 

235 # Generate with wrong body 

236 response = await client.post( 

237 "/yolo", 

238 json={"foo": "bar"}) 

239 assert response.status_code == HTTPStatus.BAD_REQUEST 

240 

241 # Generate without arguments 

242 response = await client.post( 

243 "/yolo", 

244 json={ 

245 "args": {}, 

246 "job_id": "job0", 

247 }, 

248 headers={"Content-Type": "application/json"}) 

249 assert response.status_code == HTTPStatus.BAD_REQUEST 

250 # TODO need to fix this 

251 

252 

253@pytest.mark.asyncio 

254async def test_index() -> None: 

255 """Check the index endpoint.""" 

256 app = run_httpserver.app 

257 client = app.test_client() 

258 response = await client.get("/") 

259 assert response.status_code == HTTPStatus.OK 

260 index_html = await response.get_data(as_text=True) 

261 assert index_html is not None 

262 assert "LMM Models" in index_html 

263 

264 

265@pytest.mark.asyncio 

266async def test_timestamps() -> None: 

267 """Check the timestamps endpoint.""" 

268 app = run_httpserver.app 

269 client = app.test_client() 

270 response = await client.get("/timestamps") 

271 assert response.status_code == HTTPStatus.OK 

272 timestamps = await response.json 

273 assert timestamps == {} 

274 

275 run_httpserver.models = {"mockmodel": MockModelGeneration()} 

276 response = await client.get("/timestamps") 

277 assert response.status_code == HTTPStatus.OK 

278 timestamps = await response.json 

279 assert timestamps is not None 

280 assert "mockmodel" in timestamps 

281 

282 

283@pytest.mark.asyncio 

284async def test_health() -> None: 

285 """Check the health endpoint.""" 

286 app = run_httpserver.app 

287 client = app.test_client() 

288 response = await client.get("/health") 

289 assert response.status_code == HTTPStatus.OK 

290 health = await response.json 

291 assert health == {} 

292 

293 run_httpserver.models = {"mockmodel": MockModelGeneration()} 

294 response = await client.get("/health") 

295 assert response.status_code == HTTPStatus.OK 

296 health = await response.json 

297 assert health is not None 

298 assert "mockmodel" in health 

299 

300 

301def test_setup_dist_environment_non_mig() -> None: 

302 """setup_dist_environment uses local_rank as device_id when multiple GPUs are visible (non-MIG).""" 

303 env = { 

304 "MASTER_ADDR": "localhost", 

305 "MASTER_PORT": "12355", 

306 "RANK": "1", 

307 "LOCAL_RANK": "1", 

308 "WORLD_SIZE": "2", 

309 "LOCAL_WORLD_SIZE": "2", 

310 } 

311 with patch.dict(os.environ, env, clear=False): 

312 mock_torch.cuda.is_available.return_value = True 

313 mock_torch.cuda.device_count.return_value = 2 

314 run_httpserver.setup_dist_environment() 

315 mock_torch.cuda.set_device.assert_called_with(1) 

316 

317 

318def test_setup_dist_environment_mig() -> None: 

319 """setup_dist_environment uses device 0 when MIG restricts container to a single visible device.""" 

320 env = { 

321 "MASTER_ADDR": "localhost", 

322 "MASTER_PORT": "12355", 

323 "RANK": "1", 

324 "LOCAL_RANK": "1", 

325 "WORLD_SIZE": "2", 

326 "LOCAL_WORLD_SIZE": "2", 

327 } 

328 with patch.dict(os.environ, env, clear=False): 

329 mock_torch.cuda.is_available.return_value = True 

330 # MIG: each process sees only its own MIG instance (device_count=1) 

331 mock_torch.cuda.device_count.return_value = 1 

332 run_httpserver.setup_dist_environment() 

333 # local_rank=1 is out of range; should fall back to device 0 

334 mock_torch.cuda.set_device.assert_called_with(0) 

335 

336 

337@pytest.mark.asyncio 

338@pytest.mark.timeout(10) 

339async def test_run_httpserver_http(monkeypatch: pytest.MonkeyPatch) -> None: 

340 """run_httpserver() configures Hypercorn without SSL when no certfile is given.""" 

341 mock_serve = AsyncMock() 

342 monkeypatch.setattr(run_httpserver, "serve", mock_serve) 

343 await run_httpserver.run_httpserver(host="127.0.0.1", port=9999) 

344 mock_serve.assert_awaited_once() 

345 config = mock_serve.call_args[0][1] 

346 assert config.bind == ["127.0.0.1:9999"] 

347 # No SSL attributes set 

348 assert getattr(config, "certfile", None) is None 

349 assert getattr(config, "keyfile", None) is None 

350 

351 

352@pytest.mark.asyncio 

353@pytest.mark.timeout(10) 

354async def test_run_httpserver_https(monkeypatch: pytest.MonkeyPatch) -> None: 

355 """run_httpserver() configures Hypercorn with SSL when certfile and keyfile are given.""" 

356 mock_serve = AsyncMock() 

357 monkeypatch.setattr(run_httpserver, "serve", mock_serve) 

358 await run_httpserver.run_httpserver( 

359 host="127.0.0.1", 

360 port=9999, 

361 certfile="/tmp/cert.pem", 

362 keyfile="/tmp/key.pem", 

363 ) 

364 mock_serve.assert_awaited_once() 

365 config = mock_serve.call_args[0][1] 

366 assert config.bind == ["127.0.0.1:9999"] 

367 assert config.certfile == "/tmp/cert.pem" 

368 assert config.keyfile == "/tmp/key.pem" 

369 

370 

371def test_arg_parsing_https_args() -> None: 

372 """arg_parsing() accepts --certfile and --keyfile arguments.""" 

373 with patch("sys.argv", ["run_httpserver", "--mock", "--certfile", "/tmp/cert.pem", "--keyfile", "/tmp/key.pem"]): 

374 args, _ = run_httpserver.arg_parsing() 

375 assert args.certfile == "/tmp/cert.pem" 

376 assert args.keyfile == "/tmp/key.pem" 

377 

378 

379def test_arg_parsing_https_defaults() -> None: 

380 """arg_parsing() defaults certfile and keyfile to None (HTTP mode).""" 

381 with patch("sys.argv", ["run_httpserver", "--mock"]): 

382 args, _ = run_httpserver.arg_parsing() 

383 assert args.certfile is None 

384 assert args.keyfile is None