|
1 | 1 | """Unit tests for Qwen3CrossEncoder model registration and scoring logic.""" |
2 | 2 |
|
| 3 | +from unittest.mock import MagicMock, patch |
| 4 | + |
3 | 5 | import numpy as np |
| 6 | +import pytest |
4 | 7 |
|
| 8 | +from qwen3_embed.common.onnx_model import OnnxOutputContext |
5 | 9 | from qwen3_embed.rerank.cross_encoder.qwen3_cross_encoder import ( |
6 | 10 | DEFAULT_INSTRUCTION, |
7 | 11 | SYSTEM_PROMPT, |
@@ -197,3 +201,118 @@ def test_token_ids_are_positive(self): |
197 | 201 |
|
198 | 202 | def test_token_ids_are_different(self): |
199 | 203 | assert TOKEN_YES_ID != TOKEN_NO_ID |
| 204 | + |
| 205 | + |
| 206 | +@pytest.fixture |
| 207 | +def mocked_qwen3_encoder(): |
| 208 | + """Returns a Qwen3CrossEncoder with mocked model and tokenizer.""" |
| 209 | + with patch( |
| 210 | + "qwen3_embed.rerank.cross_encoder.qwen3_cross_encoder.Qwen3CrossEncoder.download_model", |
| 211 | + return_value="/tmp/mock", |
| 212 | + ): |
| 213 | + encoder = Qwen3CrossEncoder("n24q02m/Qwen3-Reranker-0.6B-ONNX-YesNo", lazy_load=True) |
| 214 | + |
| 215 | + encoder.model = MagicMock() |
| 216 | + # Mocking output of the ONNX model to be (batch=1, 2) shape |
| 217 | + encoder.model.run.return_value = [np.array([[-10.0, 10.0]], dtype=np.float32)] |
| 218 | + encoder.model_input_names = {"input_ids", "attention_mask"} |
| 219 | + |
| 220 | + mock_tokenizer = MagicMock() |
| 221 | + mock_encoding = MagicMock() |
| 222 | + mock_encoding.ids = [1, 2, 3] |
| 223 | + mock_encoding.attention_mask = [1, 1, 1] |
| 224 | + mock_tokenizer.encode_batch.return_value = [mock_encoding] |
| 225 | + encoder.tokenizer = mock_tokenizer |
| 226 | + |
| 227 | + return encoder |
| 228 | + |
| 229 | + |
| 230 | +class TestQwen3CrossEncoderInference: |
| 231 | + """Verify ONNX embedding methods override.""" |
| 232 | + |
| 233 | + def test_onnx_embed_texts_missing_model(self): |
| 234 | + with patch( |
| 235 | + "qwen3_embed.rerank.cross_encoder.qwen3_cross_encoder.Qwen3CrossEncoder.download_model", |
| 236 | + return_value="/tmp/mock", |
| 237 | + ): |
| 238 | + encoder = Qwen3CrossEncoder("n24q02m/Qwen3-Reranker-0.6B-ONNX-YesNo", lazy_load=True) |
| 239 | + encoder.model = None |
| 240 | + with pytest.raises(ValueError, match="Model not loaded"): |
| 241 | + encoder._onnx_embed_texts(["text1"]) |
| 242 | + |
| 243 | + def test_onnx_embed_texts_missing_tokenizer(self): |
| 244 | + with patch( |
| 245 | + "qwen3_embed.rerank.cross_encoder.qwen3_cross_encoder.Qwen3CrossEncoder.download_model", |
| 246 | + return_value="/tmp/mock", |
| 247 | + ): |
| 248 | + encoder = Qwen3CrossEncoder("n24q02m/Qwen3-Reranker-0.6B-ONNX-YesNo", lazy_load=True) |
| 249 | + encoder.model = MagicMock() |
| 250 | + encoder.tokenizer = None |
| 251 | + with pytest.raises(AssertionError, match="Tokenizer not loaded"): |
| 252 | + encoder._onnx_embed_texts(["text1"]) |
| 253 | + |
| 254 | + def test_onnx_embed_texts_success(self, mocked_qwen3_encoder): |
| 255 | + ctx = mocked_qwen3_encoder._onnx_embed_texts(["hello world"]) |
| 256 | + assert isinstance(ctx, OnnxOutputContext) |
| 257 | + assert ctx.model_output.shape == (1,) |
| 258 | + # -10.0, 10.0 should give high probability |
| 259 | + assert ctx.model_output[0] > 0.99 |
| 260 | + mocked_qwen3_encoder.model.run.assert_called_once() |
| 261 | + mocked_qwen3_encoder.tokenizer.encode_batch.assert_called_once_with(["hello world"]) |
| 262 | + |
| 263 | + def test_onnx_embed_texts_multiple(self, mocked_qwen3_encoder): |
| 264 | + # We simulate tokenizer returning for a single text, because _onnx_embed_texts loops text by text |
| 265 | + ctx = mocked_qwen3_encoder._onnx_embed_texts(["text1", "text2"]) |
| 266 | + assert ctx.model_output.shape == (2,) |
| 267 | + assert mocked_qwen3_encoder.model.run.call_count == 2 |
| 268 | + assert mocked_qwen3_encoder.tokenizer.encode_batch.call_count == 2 |
| 269 | + |
| 270 | + def test_onnx_embed_pairs_success(self, mocked_qwen3_encoder): |
| 271 | + ctx = mocked_qwen3_encoder.onnx_embed_pairs([("Query1", "Doc1"), ("Query2", "Doc2")]) |
| 272 | + assert ctx.model_output.shape == (2,) |
| 273 | + assert mocked_qwen3_encoder.model.run.call_count == 2 |
| 274 | + assert mocked_qwen3_encoder.tokenizer.encode_batch.call_count == 2 |
| 275 | + |
| 276 | + # Verify chat template formatting |
| 277 | + calls = mocked_qwen3_encoder.tokenizer.encode_batch.call_args_list |
| 278 | + # Call 1 |
| 279 | + text1 = calls[0][0][0][0] |
| 280 | + assert "<Query>: Query1" in text1 |
| 281 | + assert "<Document>: Doc1" in text1 |
| 282 | + # Call 2 |
| 283 | + text2 = calls[1][0][0][0] |
| 284 | + assert "<Query>: Query2" in text2 |
| 285 | + assert "<Document>: Doc2" in text2 |
| 286 | + |
| 287 | + def test_onnx_embed_success(self, mocked_qwen3_encoder): |
| 288 | + ctx = mocked_qwen3_encoder.onnx_embed("Query", ["Doc1", "Doc2"]) |
| 289 | + assert ctx.model_output.shape == (2,) |
| 290 | + assert mocked_qwen3_encoder.model.run.call_count == 2 |
| 291 | + assert mocked_qwen3_encoder.tokenizer.encode_batch.call_count == 2 |
| 292 | + |
| 293 | + calls = mocked_qwen3_encoder.tokenizer.encode_batch.call_args_list |
| 294 | + text1 = calls[0][0][0][0] |
| 295 | + assert "<Query>: Query" in text1 |
| 296 | + assert "<Document>: Doc1" in text1 |
| 297 | + |
| 298 | + text2 = calls[1][0][0][0] |
| 299 | + assert "<Query>: Query" in text2 |
| 300 | + assert "<Document>: Doc2" in text2 |
| 301 | + |
| 302 | + def test_onnx_embed_custom_instruction(self, mocked_qwen3_encoder): |
| 303 | + ctx = mocked_qwen3_encoder.onnx_embed("Query", ["Doc"], instruction="Custom Instruction!") |
| 304 | + assert ctx.model_output.shape == (1,) |
| 305 | + |
| 306 | + calls = mocked_qwen3_encoder.tokenizer.encode_batch.call_args_list |
| 307 | + text1 = calls[0][0][0][0] |
| 308 | + assert "<Instruct>: Custom Instruction!" in text1 |
| 309 | + |
| 310 | + def test_onnx_embed_pairs_custom_instruction(self, mocked_qwen3_encoder): |
| 311 | + ctx = mocked_qwen3_encoder.onnx_embed_pairs( |
| 312 | + [("Query", "Doc")], instruction="Custom Pair Instruction!" |
| 313 | + ) |
| 314 | + assert ctx.model_output.shape == (1,) |
| 315 | + |
| 316 | + calls = mocked_qwen3_encoder.tokenizer.encode_batch.call_args_list |
| 317 | + text1 = calls[0][0][0][0] |
| 318 | + assert "<Instruct>: Custom Pair Instruction!" in text1 |
0 commit comments