Merge pull request #301 from ggozad/chore/test-cleanup
Test suite cleanup & parallelization
This commit is contained in:
commit
24de604678
14 changed files with 103 additions and 15143 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -68,8 +68,6 @@ jobs:
|
|||
run: uv run python -c "from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('Qwen/Qwen3-Embedding-0.6B')"
|
||||
- name: Run tests with coverage
|
||||
run: uv run pytest -m "not integration" --cov=haiku --cov-report=xml
|
||||
env:
|
||||
HF_HUB_OFFLINE: "1"
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Test suite cleanup**: Removed stale VCR cassettes, dead fixtures, orphaned directories, and redundant tests. Strengthened weak assertions across search, context enhancement, and converter tests. Relocated misplaced `SearchResult._get_primary_label` test to `test_search.py`
|
||||
- **Parallel test execution**: Added `pytest-xdist` and enabled parallel test runs by default (`-n auto`), reducing test suite time from ~3.5 min to ~2 min
|
||||
|
||||
## [0.33.0] - 2026-03-04
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ dev = [
|
|||
"pytest-asyncio>=1.3.0",
|
||||
"pytest-cov>=7.0.0",
|
||||
"pytest-recording>=0.13.4",
|
||||
"pytest-xdist>=3.0",
|
||||
"ruff>=0.14.13",
|
||||
]
|
||||
|
||||
|
|
@ -120,6 +121,7 @@ python = ".venv"
|
|||
[tool.pytest.ini_options]
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
asyncio_mode = "auto"
|
||||
addopts = "-n auto"
|
||||
testpaths = ["tests"]
|
||||
norecursedirs = ["examples", "docs", "evaluations", ".git", ".venv"]
|
||||
markers = [
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -17,13 +17,14 @@ embeddings:
|
|||
vector_dim: 2560
|
||||
""")
|
||||
os.environ["HAIKU_RAG_CONFIG_PATH"] = str(_test_config_path)
|
||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||
|
||||
import pydantic_ai.models # noqa: E402
|
||||
import pytest # noqa: E402
|
||||
import yaml # noqa: E402
|
||||
from datasets import Dataset, load_dataset, load_from_disk # noqa: E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datasets import Dataset
|
||||
from vcr import VCR
|
||||
|
||||
setattr(pydantic_ai.models, "ALLOW_MODEL_REQUESTS", False)
|
||||
|
|
@ -31,7 +32,9 @@ logging.getLogger("vcr.cassette").setLevel(logging.WARNING)
|
|||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def qa_corpus() -> Dataset:
|
||||
def qa_corpus() -> "Dataset":
|
||||
from datasets import Dataset, load_dataset, load_from_disk
|
||||
|
||||
ds_path = Path(__file__).parent / "data" / "dataset"
|
||||
ds_path.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,17 +1,9 @@
|
|||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from haiku.rag.agents.research.models import ResearchReport
|
||||
from haiku.rag.client import HaikuRAG
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vcr_cassette_dir():
|
||||
return str(Path(__file__).parent / "cassettes" / "test_client_research")
|
||||
|
||||
|
||||
async def test_client_research_report(temp_db_path):
|
||||
"""Test client.research() delegates to research graph in report mode."""
|
||||
mock_report = ResearchReport(
|
||||
|
|
|
|||
|
|
@ -335,60 +335,6 @@ async def test_max_items_limit_caps_expansion(temp_db_path):
|
|||
assert item_count <= 2, f"Expected at most 2 items, got {item_count}"
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
async def test_search_result_get_primary_label():
|
||||
"""Test _get_primary_label prioritizes structural labels correctly."""
|
||||
# Table should be prioritized
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c1",
|
||||
document_id="d1",
|
||||
labels=["paragraph", "table", "text"],
|
||||
)
|
||||
assert result._get_primary_label() == "table"
|
||||
|
||||
# Code should be prioritized over paragraph
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c2",
|
||||
document_id="d2",
|
||||
labels=["paragraph", "code"],
|
||||
)
|
||||
assert result._get_primary_label() == "code"
|
||||
|
||||
# list_item should be prioritized
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c3",
|
||||
document_id="d3",
|
||||
labels=["text", "list_item"],
|
||||
)
|
||||
assert result._get_primary_label() == "list_item"
|
||||
|
||||
# Returns first label when no priority match
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c4",
|
||||
document_id="d4",
|
||||
labels=["paragraph", "text"],
|
||||
)
|
||||
assert result._get_primary_label() == "paragraph"
|
||||
|
||||
# Returns None for empty labels
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c5",
|
||||
document_id="d5",
|
||||
labels=[],
|
||||
)
|
||||
assert result._get_primary_label() is None
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
async def test_expand_context_radius_zero(temp_db_path):
|
||||
"""Test expand_context with radius 0 returns original results."""
|
||||
|
|
@ -696,6 +642,10 @@ First paragraph of results.
|
|||
assert len(r.content) > 0
|
||||
# Score should be preserved (best score)
|
||||
assert r.score in [0.9, 0.8]
|
||||
# Expanded content should have docling refs
|
||||
assert r.doc_item_refs is not None and len(r.doc_item_refs) > 0
|
||||
# Document has headings, expanded result should too
|
||||
assert r.headings is not None and len(r.headings) > 0
|
||||
|
||||
|
||||
def create_picture_document() -> DoclingDocument:
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@ class TestDoclingServeConverterIntegration:
|
|||
|
||||
doc = await converter.convert_file(pdf_path)
|
||||
assert isinstance(doc, DoclingDocument)
|
||||
assert len(doc.pages) > 0
|
||||
assert len(doc.export_to_markdown().strip()) > 100
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="docling-serve does not return picture image data in JSON response "
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import httpx
|
||||
|
|
@ -8,11 +7,6 @@ import pytest
|
|||
from haiku.rag.client import HaikuRAG
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vcr_cassette_dir():
|
||||
return str(Path(__file__).parent / "cassettes" / "test_download_models")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_to_thread():
|
||||
"""Patch asyncio.to_thread to skip docling/tokenizer downloads."""
|
||||
|
|
|
|||
|
|
@ -154,8 +154,7 @@ async def test_embed_chunks_returns_new_objects(allow_model_requests):
|
|||
assert embedded[0] is not original
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
async def test_embed_chunks_empty_list(allow_model_requests):
|
||||
async def test_embed_chunks_empty_list():
|
||||
"""Test that embed_chunks handles empty list."""
|
||||
result = await embed_chunks([])
|
||||
assert result == []
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
|
|||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
async def test_chunks_include_document_info(temp_db_path):
|
||||
"""Test that search results include document URI and metadata."""
|
||||
async def test_search_chunk_includes_document_provenance(temp_db_path):
|
||||
"""Test that raw chunk search results include document URI, metadata, and ID."""
|
||||
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||
|
||||
# Create a document with URI and metadata
|
||||
# Create a document with URI and metadata but no title
|
||||
created_document = await client.create_document(
|
||||
content="This is a test document with some content for searching.",
|
||||
uri="https://example.com/test.html",
|
||||
|
|
@ -87,32 +87,7 @@ async def test_chunks_include_document_info(temp_db_path):
|
|||
assert chunk.document_uri == "https://example.com/test.html"
|
||||
assert chunk.document_meta == {"title": "Test Document", "author": "Test Author"}
|
||||
assert chunk.document_id == created_document.id
|
||||
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
async def test_chunks_include_document_title(temp_db_path):
|
||||
"""Test that search results include the parent document title when present."""
|
||||
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||
|
||||
# Create a document with URI and title
|
||||
await client.create_document(
|
||||
content="This is a test document with a custom title to verify enrichment.",
|
||||
uri="file:///tmp/title-test.md",
|
||||
title="My Custom Title",
|
||||
)
|
||||
|
||||
# Perform a search that should find this document
|
||||
results = await client.chunk_repository.search(
|
||||
"custom title", limit=3, search_type="hybrid"
|
||||
)
|
||||
|
||||
assert results, "Expected at least one search result"
|
||||
for chunk, _ in results:
|
||||
# All returned chunks for this doc should carry the document title
|
||||
if chunk.document_uri == "file:///tmp/title-test.md":
|
||||
assert chunk.document_title == "My Custom Title"
|
||||
assert chunk.document_title is None
|
||||
|
||||
client.close()
|
||||
|
||||
|
|
@ -207,9 +182,12 @@ async def test_search_returns_search_result(temp_db_path):
|
|||
assert result.score > 0
|
||||
assert result.document_uri == "https://example.com/ml.html"
|
||||
assert result.document_title == "ML Guide"
|
||||
assert result.chunk_id is not None
|
||||
assert result.document_id is not None
|
||||
# page_numbers and headings come from chunk metadata
|
||||
assert isinstance(result.page_numbers, list)
|
||||
assert isinstance(result.labels, list)
|
||||
assert len(result.labels) > 0
|
||||
|
||||
client.close()
|
||||
|
||||
|
|
@ -272,3 +250,56 @@ async def test_search_result_format_includes_metadata(temp_db_path):
|
|||
# Should include content
|
||||
assert "Content:" in formatted
|
||||
assert "machine learning" in formatted.lower()
|
||||
|
||||
|
||||
def test_search_result_primary_label_prioritizes_structural_types():
|
||||
"""Test _get_primary_label prioritizes structural labels correctly."""
|
||||
# Table should be prioritized
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c1",
|
||||
document_id="d1",
|
||||
labels=["paragraph", "table", "text"],
|
||||
)
|
||||
assert result._get_primary_label() == "table"
|
||||
|
||||
# Code should be prioritized over paragraph
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c2",
|
||||
document_id="d2",
|
||||
labels=["paragraph", "code"],
|
||||
)
|
||||
assert result._get_primary_label() == "code"
|
||||
|
||||
# list_item should be prioritized
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c3",
|
||||
document_id="d3",
|
||||
labels=["text", "list_item"],
|
||||
)
|
||||
assert result._get_primary_label() == "list_item"
|
||||
|
||||
# Returns first label when no priority match
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c4",
|
||||
document_id="d4",
|
||||
labels=["paragraph", "text"],
|
||||
)
|
||||
assert result._get_primary_label() == "paragraph"
|
||||
|
||||
# Returns None for empty labels
|
||||
result = SearchResult(
|
||||
content="test",
|
||||
score=0.5,
|
||||
chunk_id="c5",
|
||||
document_id="d5",
|
||||
labels=[],
|
||||
)
|
||||
assert result._get_primary_label() is None
|
||||
|
|
|
|||
24
uv.lock
24
uv.lock
|
|
@ -984,6 +984,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.2.1"
|
||||
|
|
@ -1365,6 +1374,7 @@ dev = [
|
|||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-recording" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ty" },
|
||||
]
|
||||
|
|
@ -1391,6 +1401,7 @@ dev = [
|
|||
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-recording", specifier = ">=0.13.4" },
|
||||
{ name = "pytest-xdist", specifier = ">=3.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.13" },
|
||||
{ name = "ty", specifier = ">=0.0.16" },
|
||||
]
|
||||
|
|
@ -4063,6 +4074,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723, upload-time = "2025-05-08T10:41:09.684Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "execnet" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue