Skip to content

Commit e18f061

Browse files
committed
Add tests for error handling and edge cases to improve coverage
1 parent 9dfa969 commit e18f061

File tree

2 files changed

+263
-0
lines changed

2 files changed

+263
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Tests for error handling in the query module's reranker integration."""
2+
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
import pytest
6+
from chromadb.api.models.AsyncCollection import AsyncCollection
7+
8+
from vectorcode.cli_utils import Config, QueryInclude
9+
from vectorcode.subcommands.query import get_query_result_files
10+
11+
12+
@pytest.fixture
13+
def mock_collection():
14+
collection = AsyncMock(spec=AsyncCollection)
15+
collection.count.return_value = 10
16+
collection.query.return_value = {
17+
"ids": [["id1", "id2", "id3"]],
18+
"distances": [[0.1, 0.2, 0.3]],
19+
"metadatas": [
20+
[
21+
{"path": "file1.py", "start": 1, "end": 1},
22+
{"path": "file2.py", "start": 1, "end": 1},
23+
{"path": "file3.py", "start": 1, "end": 1},
24+
],
25+
],
26+
"documents": [
27+
["content1", "content2", "content3"],
28+
],
29+
}
30+
return collection
31+
32+
33+
@pytest.fixture
34+
def mock_config():
35+
return Config(
36+
query=["test query"],
37+
n_result=3,
38+
query_multiplier=2,
39+
chunk_size=100,
40+
overlap_ratio=0.2,
41+
project_root="/test/project",
42+
pipe=False,
43+
include=[QueryInclude.path, QueryInclude.document],
44+
query_exclude=[],
45+
reranker=None,
46+
reranker_params={},
47+
use_absolute_path=False,
48+
)
49+
50+
51+
@pytest.mark.asyncio
52+
async def test_get_query_result_files_registry_error(mock_collection, mock_config):
53+
"""Test graceful handling of a reranker not found in registry."""
54+
# Set a custom reranker to trigger the error path
55+
mock_config.reranker = "custom-reranker"
56+
57+
# Mock stderr to capture error messages
58+
with patch("sys.stderr") as mock_stderr:
59+
# Mock the NaiveReranker for fallback
60+
with patch("vectorcode.subcommands.query.reranker.NaiveReranker") as mock_naive:
61+
mock_reranker_instance = MagicMock()
62+
mock_reranker_instance.rerank.return_value = ["file1.py", "file2.py"]
63+
mock_naive.return_value = mock_reranker_instance
64+
65+
# This should fall back to NaiveReranker
66+
result = await get_query_result_files(mock_collection, mock_config)
67+
68+
# Verify the error was logged
69+
assert mock_stderr.write.called
70+
assert "not found in registry" in "".join(
71+
[c[0][0] for c in mock_stderr.write.call_args_list]
72+
)
73+
74+
# Verify fallback to NaiveReranker happened
75+
assert mock_naive.called
76+
77+
# Check the result contains the expected files
78+
assert result == ["file1.py", "file2.py"]
79+
80+
81+
@pytest.mark.asyncio
82+
async def test_get_query_result_files_general_exception(mock_collection, mock_config):
83+
"""Test handling of unexpected exceptions during reranker loading."""
84+
# Set a custom reranker to trigger the import path
85+
mock_config.reranker = "buggy-reranker"
86+
87+
# Create a patching context that raises an unexpected exception
88+
with patch("vectorcode.rerankers", new=MagicMock()) as mock_rerankers:
89+
# Configure the mock to raise RuntimeError when create_reranker is called
90+
mock_rerankers.create_reranker.side_effect = RuntimeError("Unexpected error")
91+
92+
# Mock stderr to capture error messages
93+
with patch("sys.stderr") as mock_stderr:
94+
# Mock the NaiveReranker for fallback
95+
with patch(
96+
"vectorcode.subcommands.query.reranker.NaiveReranker"
97+
) as mock_naive:
98+
mock_reranker_instance = MagicMock()
99+
mock_reranker_instance.rerank.return_value = ["file1.py", "file2.py"]
100+
mock_naive.return_value = mock_reranker_instance
101+
102+
# This should catch the exception and fall back to NaiveReranker
103+
result = await get_query_result_files(mock_collection, mock_config)
104+
105+
# Verify the error was logged
106+
assert mock_stderr.write.called
107+
108+
# Verify fallback to NaiveReranker happened
109+
assert mock_naive.called
110+
111+
# Check the result contains the expected files
112+
assert result == ["file1.py", "file2.py"]
113+
114+
115+
@pytest.mark.asyncio
116+
async def test_get_query_result_files_cross_encoder_error(mock_collection, mock_config):
117+
"""Test the CrossEncoder special case with error handling."""
118+
# Set a cross encoder model to trigger that code path
119+
mock_config.reranker = "cross-encoder/model-name"
120+
121+
# Mock the CrossEncoderReranker to raise an exception
122+
with patch(
123+
"vectorcode.subcommands.query.reranker.CrossEncoderReranker"
124+
) as mock_cross_encoder:
125+
mock_cross_encoder.side_effect = ValueError("Model not found")
126+
127+
# Mock stderr to capture error messages
128+
with patch("sys.stderr") as mock_stderr:
129+
# Mock the NaiveReranker for fallback
130+
with patch(
131+
"vectorcode.subcommands.query.reranker.NaiveReranker"
132+
) as mock_naive:
133+
mock_reranker_instance = MagicMock()
134+
mock_reranker_instance.rerank.return_value = ["file1.py", "file2.py"]
135+
mock_naive.return_value = mock_reranker_instance
136+
137+
# This should catch the exception and fall back
138+
result = await get_query_result_files(mock_collection, mock_config)
139+
140+
# Verify the error was logged
141+
assert mock_stderr.write.called
142+
143+
# Verify fallback to NaiveReranker happened
144+
assert mock_naive.called
145+
146+
# Check the result contains the expected files
147+
assert result == ["file1.py", "file2.py"]

tests/test_reranker_edge_cases.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Tests for edge cases in the rerankers modules."""
2+
3+
from unittest.mock import patch
4+
5+
import pytest
6+
7+
from vectorcode.cli_utils import Config
8+
from vectorcode.rerankers import (
9+
CrossEncoderReranker,
10+
LlamaCppReranker,
11+
NaiveReranker,
12+
create_reranker,
13+
list_available_rerankers,
14+
)
15+
16+
17+
class TestRerankerEdgeCases:
18+
"""Tests for edge cases and error handling in reranker implementations."""
19+
20+
def test_naive_reranker_none_path(self):
21+
"""Test NaiveReranker handling of None paths in metadata."""
22+
# Create a config
23+
config = Config(n_result=2)
24+
25+
# Create a reranker
26+
reranker = NaiveReranker(config)
27+
28+
# Create results with a None path in metadata
29+
results = {
30+
"ids": [["id1", "id2", "id3"]],
31+
"metadatas": [
32+
[
33+
{"path": "file1.py"},
34+
{"path": None}, # None path here
35+
{"path": "file3.py"},
36+
]
37+
],
38+
"distances": [[0.1, 0.2, 0.3]],
39+
"documents": [["doc1", "doc2", "doc3"]],
40+
}
41+
42+
# This should not raise any exceptions
43+
ranked_results = reranker.rerank(results)
44+
45+
# Verify we get valid results (excluding the None path)
46+
assert len(ranked_results) <= 2 # n_result=2
47+
assert None not in ranked_results
48+
49+
def test_create_reranker_not_found(self):
50+
"""Test error handling when a reranker can't be found."""
51+
# Try to create a reranker with a name that doesn't exist
52+
with pytest.raises(ValueError) as exc_info:
53+
create_reranker("nonexistent-reranker")
54+
55+
# Verify the error message includes available rerankers
56+
assert "not found in registry" in str(exc_info.value)
57+
assert "Available rerankers" in str(exc_info.value)
58+
59+
# Available rerankers list should be included
60+
for reranker_name in list_available_rerankers():
61+
assert reranker_name in str(exc_info.value)
62+
63+
def test_llama_cpp_reranker_empty_results(self):
64+
"""Test LlamaCppReranker with empty results."""
65+
# Create the reranker
66+
reranker = LlamaCppReranker(model_name="test-model")
67+
68+
# Mock empty results
69+
results = {"ids": [], "documents": []}
70+
71+
# This should log a warning but not crash
72+
with patch("sys.stderr") as mock_stderr:
73+
ranked_results = reranker.rerank(results)
74+
75+
# Verify warning was logged
76+
assert mock_stderr.write.called
77+
78+
# We should get empty results back
79+
assert ranked_results == []
80+
81+
def test_llama_cpp_reranker_missing_fields(self):
82+
"""Test LlamaCppReranker with missing fields in results."""
83+
# Create the reranker
84+
reranker = LlamaCppReranker(model_name="test-model")
85+
86+
# Missing 'documents' field
87+
results = {
88+
"ids": [["id1", "id2"]],
89+
# documents field is missing
90+
}
91+
92+
# This should log a warning but not crash
93+
with patch("sys.stderr") as mock_stderr:
94+
ranked_results = reranker.rerank(results)
95+
96+
# Verify warning was logged
97+
assert mock_stderr.write.called
98+
99+
# We should get empty results
100+
assert ranked_results == []
101+
102+
def test_crossencoder_validation_error(self):
103+
"""Test CrossEncoderReranker validation errors."""
104+
# Try to create a reranker without required parameters
105+
with pytest.raises(ValueError):
106+
CrossEncoderReranker()
107+
108+
# Try with model_name but no query_chunks
109+
with pytest.raises(ValueError) as exc_info:
110+
CrossEncoderReranker(model_name="cross-encoder/model")
111+
assert "query_chunks must be provided" in str(exc_info.value)
112+
113+
# Try with query_chunks but no model_name
114+
with pytest.raises(ValueError) as exc_info:
115+
CrossEncoderReranker(query_chunks=["query"])
116+
assert "model_name must be provided" in str(exc_info.value)

0 commit comments

Comments
 (0)