Spaces:
Running
Running
File size: 8,945 Bytes
1c701bb d05440d 1c701bb 6cf7e7c 1c701bb 6cf7e7c 1c701bb 6cf7e7c 1c701bb 6cf7e7c 1c701bb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | """
Fuzz tests for `MathLibRetriever.retrieve()` and `MathLibRetriever.__init__`.
Heavy/expensive deps (HuggingFaceEmbeddings, FAISS, HuggingFaceCrossEncoder) are
mocked BEFORE `retriever` is imported so the module never touches the real
models or disk-resident indices.
"""
import os
import sys
import tempfile
import unittest
from unittest import mock
# Make `src/` importable.
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
# Spec requires literal `sys.path.insert(0, 'src')` too.
sys.path.insert(0, "src")
# ---------------------------------------------------------------------------
# Patch heavy dependencies BEFORE importing retriever.
# ---------------------------------------------------------------------------
from langchain_core.documents import Document # safe import (lightweight)
# Fake documents the mocked retriever pipeline returns.
_FAKE_DOCS = [
Document(page_content=f"lemma_{i}", metadata={"name": f"lemma_{i}"})
for i in range(5)
]
class _FakeEmbeddings:
def __init__(self, *a, **kw):
pass
def embed_documents(self, texts):
return [[0.0] * 8 for _ in texts]
def embed_query(self, text):
return [0.0] * 8
class _FakeDocstore:
def __init__(self, docs):
self._dict = {str(i): d for i, d in enumerate(docs)}
class _FakeFAISS:
"""Mock for `langchain_community.vectorstores.FAISS`."""
def __init__(self, docs=None):
self.docstore = _FakeDocstore(docs or _FAKE_DOCS)
@classmethod
def from_documents(cls, docs, embeddings): # noqa: ARG003
return cls(docs)
@classmethod
def load_local(cls, path, embeddings, allow_dangerous_deserialization=False): # noqa: ARG003
# Mimic real FAISS: choke on garbage files.
index_path = os.path.join(path, "index.faiss")
if os.path.exists(index_path):
with open(index_path, "rb") as fh:
head = fh.read(64)
# Real FAISS index has a magic header; garbage will fail to parse.
if b"garbage" in head or len(head) < 8:
raise RuntimeError(f"FAISS index at {path} is corrupt or unreadable.")
return cls(_FAKE_DOCS)
def save_local(self, path):
os.makedirs(path, exist_ok=True)
with open(os.path.join(path, "index.faiss"), "wb") as fh:
fh.write(b"\x00" * 128)
def as_retriever(self, search_kwargs=None): # noqa: ARG002
return _FakeBaseRetriever()
def similarity_search(self, query, k=5): # noqa: ARG002
# New direct-FAISS retrieval path (post-LeanDojo refactor).
return list(_FAKE_DOCS)[:k]
# Real FAISS exposes `.index` so the retriever can tune `nprobe`. A bare
# object that doesn't have an `nprobe` attribute will trigger our
# try/except AttributeError fallback cleanly.
index = object()
class _FakeBaseRetriever:
def invoke(self, query): # noqa: ARG002
return list(_FAKE_DOCS)
class _FakeCrossEncoder:
def __init__(self, *a, **kw):
pass
def score(self, pairs):
return [0.5 for _ in pairs]
# Build the module-level mocks. These need to be patched in *both* the
# original module *and* the `retriever` module namespace, since `retriever.py`
# does `from X import Y` (binds Y locally).
_patchers = [
mock.patch("langchain_huggingface.HuggingFaceEmbeddings", _FakeEmbeddings),
mock.patch("langchain_community.vectorstores.FAISS", _FakeFAISS),
mock.patch(
"langchain_community.cross_encoders.HuggingFaceCrossEncoder",
_FakeCrossEncoder,
),
]
for p in _patchers:
p.start()
# Force a fresh import: another test file in the same run may have already
# imported the real `retriever` (e.g. via `app`), which would shadow our
# mocked dependencies. Dropping it from sys.modules makes the import below
# re-run with the patchers active.
sys.modules.pop("retriever", None)
import retriever # noqa: E402
# Re-bind the locally imported names inside `retriever` to our fakes, because
# `from … import …` already captured the originals at import time.
retriever.HuggingFaceEmbeddings = _FakeEmbeddings
retriever.FAISS = _FakeFAISS
retriever.HuggingFaceCrossEncoder = _FakeCrossEncoder
# ---------------------------------------------------------------------------
# Helper: build a retriever with `_retriever` pre-populated, sidestepping the
# real BM25/EnsembleRetriever/ContextualCompressionRetriever pipeline that we
# don't need for fuzzing `retrieve()`'s query handling.
# ---------------------------------------------------------------------------
def _make_retriever_with_fake_pipeline(index_dir=None):
r = retriever.MathLibRetriever(index_dir=index_dir)
r._retriever = _FakeBaseRetriever()
return r
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class RetrieveQueryFuzzTests(unittest.TestCase):
"""Fuzz the `retrieve(query)` method."""
def setUp(self):
self.r = _make_retriever_with_fake_pipeline()
def test_empty_string_does_not_crash(self):
results = self.r.retrieve("")
self.assertIsInstance(results, list)
def test_whitespace_only_does_not_crash(self):
results = self.r.retrieve(" \n ")
self.assertIsInstance(results, list)
def test_long_query_does_not_crash(self):
q = "foo " * 1250 # 5000 chars
self.assertEqual(len(q), 5000)
results = self.r.retrieve(q)
self.assertIsInstance(results, list)
def test_only_special_chars_does_not_crash(self):
q = "!@#$%^&*()_+-=[]{}|;:',.<>?/`"
results = self.r.retrieve(q)
self.assertIsInstance(results, list)
def test_lean_unicode_does_not_crash(self):
q = "∀ n : ℕ, ∃ m, n + m = m + n" # ∀ n : ℕ, ∃ m, n + m = m + n
results = self.r.retrieve(q)
self.assertIsInstance(results, list)
def test_none_query_raises_typeerror(self):
"""None as a query must raise TypeError, not return garbage."""
with self.assertRaises(TypeError):
self.r.retrieve(None)
def test_non_string_int_raises_typeerror(self):
with self.assertRaises(TypeError):
self.r.retrieve(12345) # type: ignore[arg-type]
def test_respects_k_argument(self):
results = self.r.retrieve("anything", k=2)
self.assertLessEqual(len(results), 2)
def test_default_k_truncates_to_rerank_top_k(self):
# _FAKE_DOCS has 5 entries, default rerank_top_k=5 so we expect all 5
results = self.r.retrieve("anything")
self.assertLessEqual(len(results), self.r.rerank_top_k)
class RetrieveLazyLoadTests(unittest.TestCase):
"""`retrieve()` should degrade gracefully when no index is present."""
def test_retrieve_without_built_index_returns_empty(self):
# The retriever skips RAG (returns []) rather than raising when no
# FAISS index exists, so the agent can still run LLM-only.
with tempfile.TemporaryDirectory() as tmp:
r = retriever.MathLibRetriever(index_dir=tmp)
self.assertEqual(r.retrieve("query"), [])
class InitFuzzTests(unittest.TestCase):
"""Fuzz `MathLibRetriever.__init__` and related path handling."""
def test_index_dir_none_uses_default(self):
r = retriever.MathLibRetriever(index_dir=None)
# The default lives at <repo>/data/mathlib_index.
self.assertTrue(str(r.index_dir).endswith(os.path.join("data", "mathlib_index")))
def test_index_dir_nonexistent_path_is_index_built_false(self):
r = retriever.MathLibRetriever(index_dir="/nonexistent/path/definitely-not-here")
self.assertFalse(r.is_index_built())
def test_index_dir_garbage_raises_clear_error_on_load(self):
with tempfile.TemporaryDirectory() as tmp:
# Drop a "looks like an index but is garbage" file.
garbage = os.path.join(tmp, "index.faiss")
with open(garbage, "wb") as fh:
fh.write(b"garbage" * 4)
r = retriever.MathLibRetriever(index_dir=tmp)
self.assertTrue(r.is_index_built()) # file exists, so passes the cheap check
with self.assertRaises(Exception) as ctx:
r._load()
# Must surface a meaningful error, not silently succeed.
msg = str(ctx.exception).lower()
self.assertTrue(
any(k in msg for k in ("faiss", "index", "corrupt", "unread", "load")),
f"Error message should mention the index/FAISS, got: {ctx.exception!r}",
)
def test_empty_index_dir_is_index_built_false(self):
with tempfile.TemporaryDirectory() as tmp:
r = retriever.MathLibRetriever(index_dir=tmp)
self.assertFalse(r.is_index_built())
if __name__ == "__main__":
unittest.main()
|