Skip to content

Commit

Permalink
Implement integration tests
Browse files Browse the repository at this point in the history
* Implement integration tests for models
  `claude` and `gemini`
* Implement integration tests for agents
  using `claude` and `gemini`
* Add invoke tasks to run individual tests
* Add integration tests to github CI pipeline

Add integration tests

Update

Update

Update
  • Loading branch information
cstub committed Jan 17, 2025
1 parent 6529276 commit 74bc658
Show file tree
Hide file tree
Showing 14 changed files with 543 additions and 7 deletions.
16 changes: 13 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ name: Tests

on:
push:
branches: [ main ]
branches: [ main, wip-integration-tests ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}

steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -39,7 +43,13 @@ jobs:
poetry install
pip list
- name: Run tests
- name: Run unit tests
shell: bash -l {0}
run: |
poetry run pytest tests/unit
- name: Run integration tests
shell: bash -l {0}
run: |
poetry run pytest -s tests
docker pull ghcr.io/gradion-ai/ipybox:basic
poetry run pytest tests/integration --no-flaky-report
26 changes: 24 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,36 @@ Install pre-commit hooks:
invoke precommit-install
```

Create a `.env` file with [Anthropic](https://console.anthropic.com/settings/keys) and [Gemini](https://aistudio.google.com/app/apikey) API keys:

```env title=".env"
# Required for Claude 3.5 Sonnet
ANTHROPIC_API_KEY=...
# Required for generative Google Search via Gemini 2
GOOGLE_API_KEY=...
```

Enforce coding conventions (done automatically by pre-commit hooks):

```bash
invoke cc
```

Run tests:
Run unit tests:

```bash
invoke ut
```

Run integration tests:

```bash
invoke it
```

Run all tests:

```bash
pytest -s tests
invoke test
```
15 changes: 13 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pre-commit = "^4.0"
invoke = "^2.2"
pytest = "^8.3"
pytest-asyncio = "^0.24.0"
flaky = "^3.8.1"

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
35 changes: 35 additions & 0 deletions tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from sys import platform

from invoke import task


Expand All @@ -24,3 +26,36 @@ def serve_docs(c):
@task
def deploy_docs(c):
c.run("mkdocs gh-deploy --force")


@task
def test(c, cov=False, cov_report=None):
_run_pytest(c, "tests", cov, cov_report)


@task(aliases=["ut"])
def unit_test(c, cov=False, cov_report=None):
_run_pytest(c, "tests/unit", cov, cov_report)


@task(aliases=["it"])
def integration_test(c, cov=False, cov_report=None):
_run_pytest(c, "tests/integration", cov, cov_report)


def _run_pytest(c, test_dir, cov=False, cov_report=None):
c.run(f"pytest {test_dir} {_pytest_cov_options(cov, cov_report)} --no-flaky-report", pty=_use_pty())


def _use_pty():
return platform != "win32"


def _pytest_cov_options(use_cov: bool, cov_reports: str | None):
if not use_cov:
return ""

cov_report_types = cov_reports.split(",") if cov_reports else []
cov_report_types = ["term"] + cov_report_types
cov_report_params = [f"--cov-report {r}" for r in cov_report_types]
return f"--cov {' '.join(cov_report_params)}"
3 changes: 3 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from pathlib import Path

TEST_ROOT_PATH = Path(__file__).parent.resolve()
Empty file added tests/helpers/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions tests/helpers/flaky.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import time

from google import genai


def rerun_on_google_genai_resource_exhausted(wait_time_s: float):
def _filter(err, name, test, plugin):
err_class, err_value, _ = err
match err_class:
case genai.errors.ClientError:
time.sleep(wait_time_s)
return "RESOURCE_EXHAUSTED" in str(err_value)
case _:
return False

return _filter
Empty file.
38 changes: 38 additions & 0 deletions tests/helpers/skills/user_repository/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from abc import ABC, abstractmethod


class UserRepository(ABC):
@abstractmethod
def find_user_name(self, user_id: str) -> str:
"""Finds the name of a user in the user repository.
Args:
user_id (str): The id of the user to find.
Returns:
str: The name of the user.
"""
pass

@abstractmethod
def find_user_email(self, user_id: str, invalidate_cache: bool = False) -> str:
"""Finds the email of a user in the user repository.
Args:
user_id (str): The id of the user to find.
invalidate_cache (bool): Whether to invalidate all the caches before lookup.
Should typically be left as False unless explicitly needed.
Returns:
str: The email of the user.
"""
pass


def create_user_repository() -> UserRepository:
"""
Creates a new instance of the UserRepository tool.
"""
from .impl import UserRepositoryImpl

return UserRepositoryImpl()
20 changes: 20 additions & 0 deletions tests/helpers/skills/user_repository/impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from .api import UserRepository

USER_ID = "user-123"


class UserRepositoryImpl(UserRepository):
def find_user_name(self, user_id: str) -> str:
if user_id.lower().strip() == USER_ID:
return "user_a37c1f54"

raise ValueError(f"User {user_id} not found")

def find_user_email(self, user_id: str, invalidate_cache: bool = False) -> str:
if not invalidate_cache:
raise ValueError("You must invalidate the cache to get the email address")

if user_id.lower().strip() == USER_ID:
return "[email protected]"

raise ValueError(f"User {user_id} not found")
41 changes: 41 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from unittest.mock import AsyncMock, MagicMock

import pytest
from dotenv import load_dotenv

from freeact.logger import Logger
from freeact.model.claude.model import Claude
from freeact.model.gemini.model.chat import Gemini


@pytest.fixture(autouse=True)
def load_env():
load_dotenv()


@pytest.fixture
def logger():
logger = MagicMock(spec=Logger)
logger.context = MagicMock()
logger.context.return_value.__aenter__ = AsyncMock()
logger.context.return_value.__aexit__ = AsyncMock()
logger.log = AsyncMock()
return logger


@pytest.fixture
def claude(logger):
return Claude(
logger=logger,
model_name="claude-3-5-haiku-20241022",
prompt_caching=False,
)


@pytest.fixture
def gemini():
return Gemini(
model_name="gemini-2.0-flash-exp",
temperature=0.0,
max_tokens=1024,
)
Loading

0 comments on commit 74bc658

Please sign in to comment.