Coverage for tests/test_wrapper_wan22.py: 100%

175 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 gc 

5import pytest 

6 

7from unittest.mock import patch 

8from unittest.mock import MagicMock 

9from unittest.mock import AsyncMock 

10 

11from PIL import Image 

12 

13from tests.torch_mock import TorchMock 

14 

15mock_torch = TorchMock() 

16 

17sys.path.append("wrapper") 

18sys.path.append("wrapper/wan") 

19sys.path.append("wrapper/wan22") 

20 

21# Build a mock for media_utils with async base64_to_audio_file 

22mock_media_utils = MagicMock() 

23mock_media_utils.base64_to_audio_file = AsyncMock(return_value="/tmp/test_audio.wav") 

24mock_media_utils.empty_audio_file = MagicMock(return_value="/tmp/warmup.wav") 

25 

26mock_modules = { 

27 'nvidia_smi': MagicMock(), 

28 'imageio': MagicMock(), 

29 'cv2': MagicMock(), 

30 'torch': mock_torch, 

31 'torchvision': MagicMock(), 

32 'torchvision.transforms': MagicMock(), 

33 'torchvision.transforms.functional': MagicMock(), 

34 'xfuser': MagicMock(), 

35 'xfuser.config': MagicMock(), 

36 'xfuser.core': MagicMock(), 

37 'xfuser.core.distributed': MagicMock(), 

38 'transformers': MagicMock(), 

39 'wan': MagicMock(), 

40 'wan.configs': MagicMock(), 

41 'wan.modules': MagicMock(), 

42 'wan.modules.t5': MagicMock(), 

43 'wan.modules.clip': MagicMock(), 

44 'wan.modules.vae': MagicMock(), 

45 'wan.modules.model': MagicMock(), 

46 'wan.utils': MagicMock(), 

47 'wan.utils.utils': MagicMock(), 

48 'wan.utils.fm_solvers_unipc': MagicMock(), 

49 'wan.distributed': MagicMock(), 

50 'wan.distributed.fsdp': MagicMock(), 

51 'wan.distributed.sequence_parallel': MagicMock(), 

52 'media_utils': mock_media_utils, 

53} 

54mock_modules.update(mock_torch.get_sub_modules()) 

55 

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

57 from image_utils import img_to_base64 

58 from wan22.wrapper_wan22 import Wan22VideoGeneration 

59 

60 

61@pytest.mark.asyncio 

62async def test_init() -> None: 

63 """Test basic initialization and health checks.""" 

64 model = Wan22VideoGeneration() 

65 assert model is not None 

66 assert model.model_name == "wan22" 

67 

68 model.init() 

69 health = model.get_health() 

70 assert health is not None 

71 timestamps = model.get_timestamps() 

72 assert timestamps is not None 

73 

74 del model 

75 gc.collect() 

76 

77 

78@pytest.mark.asyncio 

79async def test_get_rest_args_missing_body() -> None: 

80 """Test that get_rest_args raises for missing JSON body.""" 

81 model = Wan22VideoGeneration() 

82 model.init() 

83 

84 with pytest.raises(ValueError, match="Missing JSON body"): 

85 await model.get_rest_args(None) 

86 

87 

88@pytest.mark.asyncio 

89async def test_get_rest_args_missing_img() -> None: 

90 """Test that get_rest_args raises when img is absent.""" 

91 model = Wan22VideoGeneration() 

92 model.init() 

93 

94 with pytest.raises(ValueError, match="Missing 'img' parameter"): 

95 await model.get_rest_args({}) 

96 

97 

98@pytest.mark.asyncio 

99async def test_get_rest_args_missing_prompt() -> None: 

100 """Test that get_rest_args raises when prompt is absent.""" 

101 model = Wan22VideoGeneration() 

102 model.init() 

103 

104 img = Image.new("RGB", (40, 30)) 

105 img_base64 = img_to_base64(img) 

106 

107 with pytest.raises(ValueError, match="Missing 'prompt' parameter"): 

108 await model.get_rest_args({"img": img_base64}) 

109 

110 

111@pytest.mark.asyncio 

112async def test_get_rest_args_missing_audio() -> None: 

113 """Test that get_rest_args raises when audio is absent and TTS is disabled.""" 

114 model = Wan22VideoGeneration() 

115 model.init() 

116 

117 img = Image.new("RGB", (40, 30)) 

118 img_base64 = img_to_base64(img) 

119 

120 with pytest.raises(ValueError, match="Missing 'audio' parameter"): 

121 await model.get_rest_args({ 

122 "img": img_base64, 

123 "prompt": "test prompt", 

124 }) 

125 

126 

127@pytest.mark.asyncio 

128async def test_get_rest_args_with_audio() -> None: 

129 """Test get_rest_args with a base64-encoded audio file.""" 

130 model = Wan22VideoGeneration() 

131 model.init() 

132 

133 img = Image.new("RGB", (40, 30)) 

134 img_base64 = img_to_base64(img) 

135 

136 result = await model.get_rest_args({ 

137 "img": img_base64, 

138 "prompt": "test prompt", 

139 "audio": "dGVzdA==", # base64("test") - placeholder 

140 }) 

141 

142 assert "args" in result 

143 assert result["task"] == "wan22" 

144 args = result["args"] 

145 assert args["prompt"] == "test prompt" 

146 assert args["neg_prompt"] == "" 

147 assert args["enable_tts"] is False 

148 assert args["audio_path"] is not None 

149 assert args["audio_path"].endswith(".wav") 

150 assert args["max_area"] == 1024 * 704 

151 assert args["sampling_steps"] == 40 

152 assert args["infer_frames"] == 80 

153 assert args["num_clip"] is None 

154 

155 del model 

156 gc.collect() 

157 

158 

159@pytest.mark.asyncio 

160async def test_get_rest_args_with_tts() -> None: 

161 """Test get_rest_args with TTS parameters.""" 

162 model = Wan22VideoGeneration() 

163 model.init() 

164 

165 img = Image.new("RGB", (40, 30)) 

166 img_base64 = img_to_base64(img) 

167 

168 result = await model.get_rest_args({ 

169 "img": img_base64, 

170 "prompt": "Summer beach scene", 

171 "enable_tts": True, 

172 "tts_prompt_audio": "dGVzdA==", 

173 "tts_prompt_text": "Hello world", 

174 "tts_text": "Generated speech text", 

175 }) 

176 

177 args = result["args"] 

178 assert args["enable_tts"] is True 

179 assert args["tts_text"] == "Generated speech text" 

180 assert args["tts_prompt_text"] == "Hello world" 

181 assert args["tts_prompt_audio"] is not None 

182 assert args["tts_prompt_audio"].endswith(".wav") 

183 assert args["audio_path"] is None 

184 

185 del model 

186 gc.collect() 

187 

188 

189@pytest.mark.asyncio 

190async def test_get_rest_args_custom_params() -> None: 

191 """Test get_rest_args honours custom resolution, steps, and clip count.""" 

192 model = Wan22VideoGeneration() 

193 model.init() 

194 

195 img = Image.new("RGB", (40, 30)) 

196 img_base64 = img_to_base64(img) 

197 

198 result = await model.get_rest_args({ 

199 "img": img_base64, 

200 "prompt": "test", 

201 "audio": "dGVzdA==", 

202 "max_area": 720 * 1280, 

203 "sampling_steps": 20, 

204 "infer_frames": 48, 

205 "num_clip": 3, 

206 "neg_prompt": "blurry", 

207 "output_type": "video_path", 

208 }) 

209 

210 args = result["args"] 

211 assert args["max_area"] == 720 * 1280 

212 assert args["sampling_steps"] == 20 

213 assert args["infer_frames"] == 48 

214 assert args["num_clip"] == 3 

215 assert args["neg_prompt"] == "blurry" 

216 assert args["output_type"] == "video_path" 

217 

218 del model 

219 gc.collect() 

220 

221 

222@pytest.mark.asyncio 

223async def test_generate_raises_without_init() -> None: 

224 """Test that generate raises when model has not been initialized.""" 

225 model = Wan22VideoGeneration() 

226 # NOTE: intentionally NOT calling model.init() here 

227 

228 img = Image.new("RGB", (40, 30)) 

229 

230 with pytest.raises(ValueError, match="Model not initialized"): 

231 await model.generate( 

232 img=img, 

233 prompt="test prompt", 

234 audio_path="/tmp/fake_audio.wav", 

235 ) 

236 

237 del model 

238 gc.collect() 

239 

240 

241def test_assert_model_init_wan_s2v_none() -> None: 

242 """Test _assert_model_init raises when wan_s2v is None after init.""" 

243 model = Wan22VideoGeneration() 

244 model.init() 

245 model.wan_s2v = None 

246 with pytest.raises(ValueError, match="WanS2V model not initialized"): 

247 model._assert_model_init() 

248 

249 

250def test_model_compile_no_op_no_torch_compile() -> None: 

251 """Test model_compile is no-op when torch_compile=False.""" 

252 model = Wan22VideoGeneration() 

253 model.init() 

254 model.torch_compile = False 

255 model.model_compile() # should not raise 

256 

257 

258def test_model_compile_no_op_no_wan_s2v() -> None: 

259 """Test model_compile is no-op when wan_s2v is None.""" 

260 model = Wan22VideoGeneration() 

261 model.init() 

262 model.torch_compile = True 

263 model.wan_s2v = None 

264 model.model_compile() # should not raise (wan_s2v is None guard) 

265 

266 

267def test_model_compile_with_torch_compile() -> None: 

268 """Test model_compile calls torch.compile when torch_compile=True and wan_s2v is set.""" 

269 model = Wan22VideoGeneration() 

270 model.init() 

271 model.torch_compile = True 

272 assert model.wan_s2v is not None 

273 model.model_compile() # torch.compile is mocked, should not raise 

274 

275 

276@pytest.mark.asyncio 

277async def test_get_rest_args_img_not_string() -> None: 

278 """Test get_rest_args raises when img is not a string.""" 

279 model = Wan22VideoGeneration() 

280 model.init() 

281 

282 with pytest.raises(ValueError, match="'img' parameter must be a base64-encoded string"): 

283 await model.get_rest_args({"img": 12345}) 

284 

285 

286@pytest.mark.asyncio 

287async def test_get_rest_args_audio_not_string() -> None: 

288 """Test get_rest_args raises when audio is not a string.""" 

289 model = Wan22VideoGeneration() 

290 model.init() 

291 

292 img = Image.new("RGB", (40, 30)) 

293 img_base64 = img_to_base64(img) 

294 

295 with pytest.raises(ValueError, match="'audio' parameter must be a base64-encoded string"): 

296 await model.get_rest_args({ 

297 "img": img_base64, 

298 "prompt": "test", 

299 "audio": 99999, 

300 }) 

301 

302 

303@pytest.mark.asyncio 

304async def test_get_rest_args_tts_prompt_audio_not_string() -> None: 

305 """Test get_rest_args raises when tts_prompt_audio is not a string.""" 

306 model = Wan22VideoGeneration() 

307 model.init() 

308 

309 img = Image.new("RGB", (40, 30)) 

310 img_base64 = img_to_base64(img) 

311 

312 with pytest.raises(ValueError, match="'tts_prompt_audio' must be a base64-encoded string"): 

313 await model.get_rest_args({ 

314 "img": img_base64, 

315 "prompt": "test", 

316 "enable_tts": True, 

317 "tts_prompt_audio": 99999, 

318 }) 

319 

320 

321@pytest.mark.asyncio 

322async def test_get_rest_args_with_job_id() -> None: 

323 """Test get_rest_args passes a deterministic audio destination when job_id is given.""" 

324 model = Wan22VideoGeneration() 

325 model.init() 

326 

327 img = Image.new("RGB", (40, 30)) 

328 img_base64 = img_to_base64(img) 

329 

330 mock_media_utils.base64_to_audio_file.reset_mock() 

331 

332 await model.get_rest_args({ 

333 "img": img_base64, 

334 "prompt": "test", 

335 "audio": "dGVzdA==", 

336 "job_id": "myjob123", 

337 }) 

338 

339 # Verify that the audio file helper was called with the expected destination path 

340 mock_media_utils.base64_to_audio_file.assert_called_once_with( 

341 "dGVzdA==", audio_path="/tmp/myjob123.wav" 

342 )