Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ jobs:
- name: Run formatting checks
run: |
make check
- name: Run unit tests
working-directory: backend
run: |
pip install huggingface_hub[cli]
huggingface-cli download --repo-type dataset The-OpenROAD-Project/ORAssistant_RAG_Dataset --include source_list.json --local-dir data/
export GOOGLE_API_KEY="dummy-unit-test-key"
make test
- name: Populate environment variables
run: |
cp backend/.env.example backend/.env
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ jobs:
- name: Run formatting checks
run: |
make check
- name: Run unit tests
working-directory: backend
run: |
pip install huggingface_hub[cli]
huggingface-cli download --repo-type dataset The-OpenROAD-Project/ORAssistant_RAG_Dataset --include source_list.json --local-dir data/
export GOOGLE_API_KEY="dummy-unit-test-key"
make test
- name: Build Docker images
run: |
docker compose build
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ faiss_db
# frontend
node_modules
.next

# coverage
coverage.xml
report.html
.coverage
13 changes: 12 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,21 @@ init-dev: init
.PHONY: format
format:
@. .venv/bin/activate && \
ruff format
ruff format && \
ruff check --fix

.PHONY: check
check:
@. .venv/bin/activate && \
mypy . && \
ruff check

.PHONY: build-docs
build-docs:
@. .venv/bin/activate && \
python build_docs.py

.PHONY: test
test:
@. .venv/bin/activate && \
pytest
68 changes: 66 additions & 2 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ exclude = [
]
line-length = 88
indent-width = 4
target-version = "py310"
target-version = "py312"

[tool.ruff.lint]
select = ["E4", "E7", "E9","E301","E304","E305","E401","E223","E224","E242", "E", "F" ,"N", "W", "C90"]
extend-select = ["D203", "D204"]
extend-select = ["D204"]
ignore = ["E501"]
preview = true

Expand All @@ -93,3 +93,67 @@ skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = false
docstring-code-line-length = "dynamic"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--cov=src",
"--cov-report=html:htmlcov",
"--cov-report=term-missing",
"--cov-report=xml",
"--cov-fail-under=40",
"--strict-markers",
"--strict-config",
"--html=reports/report.html",
"--self-contained-html",
"-v"
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests"
]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning"
]
asyncio_mode = "auto"

[tool.coverage.run]
source = ["src"]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/venv/*",
"*/env/*",
"*/.venv/*",
"*/site-packages/*",
"*/migrations/*",
"*/post_install.py",
"*/secret.json"
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod"
]

[tool.coverage.html]
directory = "htmlcov"
title = "ORAssistant Backend Coverage Report"

[tool.coverage.xml]
output = "coverage.xml"
5 changes: 5 additions & 0 deletions backend/requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ types-tqdm==4.66.0.20240417
types-beautifulsoup4==4.12.0.20240511
ruff==0.5.1
pre-commit==3.7.1
pytest==8.3.2
pytest-cov==5.0.0
pytest-html==4.1.1
pytest-xdist==3.6.0
pytest-asyncio==0.23.8
89 changes: 89 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch
import tempfile
import os


@pytest.fixture(scope="session")
def test_data_dir():
"""Get test data directory path."""
return Path(__file__).parent / "data"


@pytest.fixture
def mock_openai_client():
"""Mock OpenAI client for testing."""
with patch("openai.OpenAI") as mock_client:
mock_instance = Mock()
mock_client.return_value = mock_instance
yield mock_instance


@pytest.fixture
def mock_langchain_llm():
"""Mock LangChain LLM for testing."""
with patch("langchain_openai.ChatOpenAI") as mock_llm:
mock_instance = Mock()
mock_llm.return_value = mock_instance
yield mock_instance


@pytest.fixture
def mock_faiss_vectorstore():
"""Mock FAISS vectorstore for testing."""
with patch("langchain_community.vectorstores.FAISS") as mock_faiss:
mock_instance = Mock()
mock_faiss.return_value = mock_instance
yield mock_instance


@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
with tempfile.TemporaryDirectory() as temp_dir:
yield Path(temp_dir)


@pytest.fixture
def sample_documents():
"""Sample documents for testing."""
return [
{
"content": "This is a sample document about OpenROAD installation.",
"metadata": {"source": "installation.md", "category": "installation"},
},
{
"content": "This document explains OpenROAD flow configuration.",
"metadata": {"source": "flow.md", "category": "configuration"},
},
]


@pytest.fixture
def mock_env_vars():
"""Mock environment variables for testing."""
env_vars = {
"OPENAI_API_KEY": "test-key",
"GOOGLE_API_KEY": "test-google-key",
"HUGGINGFACE_API_KEY": "test-hf-key",
}

with patch.dict(os.environ, env_vars):
yield env_vars


@pytest.fixture(autouse=True)
def setup_test_environment():
"""Set up test environment before each test."""
# Add src directory to Python path
src_path = Path(__file__).parent.parent / "src"
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))

yield

# Cleanup after test
if str(src_path) in sys.path:
sys.path.remove(str(src_path))
30 changes: 30 additions & 0 deletions backend/tests/data/sample.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# OpenROAD Test Document

This is a sample markdown document for testing purposes.

## Installation

OpenROAD can be installed using the following methods:

1. Build from source
2. Use Docker container
3. Install pre-built binaries

## Configuration

Configure OpenROAD using the following commands:

```tcl
set_design_name "my_design"
set_top_module "top"
```

## Flow

The OpenROAD flow consists of several stages:

- Synthesis
- Floorplanning
- Placement
- Clock Tree Synthesis
- Routing
71 changes: 71 additions & 0 deletions backend/tests/test_api_healthcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pytest
from fastapi.testclient import TestClient
from fastapi import FastAPI

from src.api.routers.healthcheck import router, HealthCheckResponse


class TestHealthCheckAPI:
"""Test suite for healthcheck API endpoints."""

@pytest.fixture
def app(self):
"""Create FastAPI application with healthcheck router."""
app = FastAPI()
app.include_router(router)
return app

@pytest.fixture
def client(self, app):
"""Create test client."""
return TestClient(app)

def test_healthcheck_endpoint_success(self, client):
"""Test healthcheck endpoint returns success response."""
response = client.get("/healthcheck")

assert response.status_code == 200
assert response.json() == {"status": "ok"}

def test_healthcheck_response_model(self):
"""Test HealthCheckResponse model."""
response = HealthCheckResponse(status="ok")

assert response.status == "ok"
assert response.model_dump() == {"status": "ok"}

def test_healthcheck_response_model_validation(self):
"""Test HealthCheckResponse model validation."""
# Test with valid status
response = HealthCheckResponse(status="healthy")
assert response.status == "healthy"

# Test with empty status
response = HealthCheckResponse(status="")
assert response.status == ""

@pytest.mark.integration
def test_healthcheck_endpoint_headers(self, client):
"""Test healthcheck endpoint response headers."""
response = client.get("/healthcheck")

assert response.status_code == 200
assert "application/json" in response.headers.get("content-type", "")

def test_healthcheck_endpoint_multiple_requests(self, client):
"""Test healthcheck endpoint handles multiple requests."""
for _ in range(5):
response = client.get("/healthcheck")
assert response.status_code == 200
assert response.json() == {"status": "ok"}

@pytest.mark.unit
@pytest.mark.asyncio
async def test_healthcheck_function_direct_call(self):
"""Test healthcheck function can be called directly."""
from src.api.routers.healthcheck import healthcheck

result = await healthcheck()

assert isinstance(result, HealthCheckResponse)
assert result.status == "ok"
Loading