diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index ff6d28a..ab53757 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -26,6 +26,45 @@ jobs: - name: "Run PyAnsys code style checks" uses: ansys/actions/code-style@v6 + tests: + name: "Tests" + runs-on: ${{ matrix.os }} + needs: [code-style] + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ['3.9', '3.12'] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Testing + uses: ansys/actions/tests-pytest@v6 + timeout-minutes: 12 + with: + checkout: false + skip-install: true + pytest-extra-args: "--cov=ansys.allie.flowkit.python --cov-report=term --cov-report=html:.cov/html --cov-report=xml:.cov/coverage.xml" + + - name: Upload coverage results (HTML) + uses: actions/upload-artifact@v4 + if: (matrix.python-version == env.MAIN_PYTHON_VERSION) && (runner.os == 'Linux') + with: + name: coverage-html + path: .cov/html + retention-days: 7 + release: name: "Release project" if: github.event_name == 'push' && contains(github.ref, 'refs/tags') diff --git a/app/fastapi_utils.py b/app/fastapi_utils.py index b700084..133644e 100644 --- a/app/fastapi_utils.py +++ b/app/fastapi_utils.py @@ -77,8 +77,8 @@ def get_parameters_info(params): if param.annotation == bytes: param_info = ParameterInfo(name=param.name, type="bytes") parameters_info.append(param_info) - elif hasattr(param.annotation, "schema"): - schema = param.annotation.schema() + elif hasattr(param.annotation, "model_json_schema"): + schema = param.annotation.model_json_schema() param_info = extract_fields_from_schema(schema) parameters_info.extend(param_info) else: @@ -101,7 +101,7 @@ def get_return_type_info(return_type: Type[BaseModel]): A list of ParameterInfo objects representing the return type fields. """ - if hasattr(return_type, "schema"): + if hasattr(return_type, "model_json_schema"): schema = return_type.model_json_schema() return extract_fields_from_schema(schema) return [ParameterInfo(name="return", type=str(return_type.__name__))] diff --git a/requirements.txt b/requirements.txt index 105af80..c17fcc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ fastapi==0.112.0 +httpx==0.27.0 langchain==0.2.12 pydantic==2.8.2 pymupdf==1.24.9 +pytest==8.3.2 +pytest-cov==5.0.0 python_pptx==1.0.1 PyYAML==6.0.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0033fb9 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +from setuptools import find_packages, setup + +setup( + name="ansys-allie-flowkit-python", + version="0.1.0", + packages=find_packages(include=["app", "docker", "configs"]), +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..f1b390f --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests module.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4f9d61d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +from unittest.mock import patch + +import pytest + +# Mock API key for testing +MOCK_API_KEY = "test_api_key" + + +@pytest.fixture(autouse=True) +def mock_api_key(): + """Mock the API key for testing.""" + with patch("app.config.CONFIG.flowkit_python_api_key", MOCK_API_KEY): + yield diff --git a/tests/test_endpoints_splitter.py b/tests/test_endpoints_splitter.py new file mode 100644 index 0000000..c947fda --- /dev/null +++ b/tests/test_endpoints_splitter.py @@ -0,0 +1,116 @@ +import base64 + +from app.app import app +from app.endpoints.splitter import validate_request +from app.models.splitter import SplitterRequest +from fastapi import HTTPException +from fastapi.testclient import TestClient +import pytest + +from tests.conftest import MOCK_API_KEY + +# Create a test client +client = TestClient(app) + + +def encode_file_to_base64(file_path): + """Encode a file to base64 string.""" + with open(file_path, "rb") as file: + return base64.b64encode(file.read()).decode("utf-8") + + +@pytest.mark.asyncio +async def test_split_ppt(): + """Test splitting text in a PowerPoint document into chunks.""" + ppt_content_base64 = encode_file_to_base64("./tests/test_files/test_presentation.pptx") + request_payload = { + "document_content": ppt_content_base64, + "chunk_size": 100, + "chunk_overlap": 10, + } + response = client.post("/splitter/ppt", json=request_payload, headers={"api-key": MOCK_API_KEY}) + if response.status_code != 200: + print(f"Response status code: {response.status_code}") + print(f"Response content: {response.json()}") + assert response.status_code == 200 + assert "chunks" in response.json() + + +@pytest.mark.asyncio +async def test_split_py(): + """Test splitting Python code into chunks.""" + python_code = """ + def hello_world(): + print("Hello, world!") + """ + python_code_base64 = base64.b64encode(python_code.encode()).decode("utf-8") + request_payload = {"document_content": python_code_base64, "chunk_size": 50, "chunk_overlap": 5} + response = client.post("/splitter/py", json=request_payload, headers={"api-key": MOCK_API_KEY}) + assert response.status_code == 200 + assert "chunks" in response.json() + + +@pytest.mark.asyncio +async def test_split_pdf(): + """Test splitting text in a PDF document into chunks.""" + pdf_content_base64 = encode_file_to_base64("./tests/test_files/test_document.pdf") + request_payload = { + "document_content": pdf_content_base64, + "chunk_size": 200, + "chunk_overlap": 20, + } + response = client.post("/splitter/pdf", json=request_payload, headers={"api-key": MOCK_API_KEY}) + assert response.status_code == 200 + assert "chunks" in response.json() + + +# Define test cases for validate_request() +validate_request_test_cases = [ + # Test case 1: valid request + ( + SplitterRequest( + document_content="dGVzdA==", chunk_size=100, chunk_overlap=10 # base64 for "test" + ), + MOCK_API_KEY, + None, + ), + # Test case: invalid API key + ( + SplitterRequest(document_content="dGVzdA==", chunk_size=100, chunk_overlap=10), + "invalid_api_key", + HTTPException(status_code=401, detail="Invalid API key"), + ), + # Test case 2: missing document content + ( + SplitterRequest(document_content="", chunk_size=100, chunk_overlap=10), + MOCK_API_KEY, + HTTPException(status_code=400, detail="No document content provided"), + ), + # Test case 4: invalid chunk size + ( + SplitterRequest(document_content="dGVzdA==", chunk_size=0, chunk_overlap=10), + MOCK_API_KEY, + HTTPException(status_code=400, detail="No chunk size provided"), + ), + # Test case 5: invalid chunk overlap + ( + SplitterRequest(document_content="dGVzdA==", chunk_size=100, chunk_overlap=-1), + MOCK_API_KEY, + HTTPException(status_code=400, detail="Chunk overlap must be greater than or equal to 0"), + ), +] + + +@pytest.mark.parametrize("api_request, api_key, expected_exception", validate_request_test_cases) +def test_validate_request(api_request, api_key, expected_exception): + """Test the validate_request function with various scenarios.""" + if expected_exception: + with pytest.raises(HTTPException) as exc_info: + validate_request(api_request, api_key) + assert exc_info.value.status_code == expected_exception.status_code + assert exc_info.value.detail == expected_exception.detail + else: + try: + validate_request(api_request, api_key) + except HTTPException: + pytest.fail("validate_request() raised HTTPException unexpectedly!") diff --git a/tests/test_files/test_document.pdf b/tests/test_files/test_document.pdf new file mode 100644 index 0000000..4898d60 Binary files /dev/null and b/tests/test_files/test_document.pdf differ diff --git a/tests/test_files/test_presentation.pptx b/tests/test_files/test_presentation.pptx new file mode 100644 index 0000000..c6eb5e7 Binary files /dev/null and b/tests/test_files/test_presentation.pptx differ diff --git a/tests/test_list_functions.py b/tests/test_list_functions.py new file mode 100644 index 0000000..c2e50b4 --- /dev/null +++ b/tests/test_list_functions.py @@ -0,0 +1,58 @@ +from app.app import app +from fastapi.testclient import TestClient +import pytest + +# Initialize the test client +client = TestClient(app) + + +@pytest.mark.asyncio +async def test_list_functions(): + """Test listing available functions.""" + # Test splitter results + response = client.get("/", headers={"api-key": "test_api_key"}) + assert response.status_code == 200 + response_data = response.json() + + expected_response_start = [ + { + "name": "split_ppt", + "path": "/splitter/ppt", + "inputs": [ + {"name": "document_content", "type": "string(binary)"}, + {"name": "chunk_size", "type": "integer"}, + {"name": "chunk_overlap", "type": "integer"}, + ], + "outputs": [{"name": "chunks", "type": "array"}], + "definitions": {}, + }, + { + "name": "split_py", + "path": "/splitter/py", + "inputs": [ + {"name": "document_content", "type": "string(binary)"}, + {"name": "chunk_size", "type": "integer"}, + {"name": "chunk_overlap", "type": "integer"}, + ], + "outputs": [{"name": "chunks", "type": "array"}], + "definitions": {}, + }, + { + "name": "split_pdf", + "path": "/splitter/pdf", + "inputs": [ + {"name": "document_content", "type": "string(binary)"}, + {"name": "chunk_size", "type": "integer"}, + {"name": "chunk_overlap", "type": "integer"}, + ], + "outputs": [{"name": "chunks", "type": "array"}], + "definitions": {}, + }, + ] + + assert response_data[:3] == expected_response_start + + # Test invalid API key + response = client.get("/", headers={"api-key": "invalid_api_key"}) + assert response.status_code == 401 + assert response.json() == {"detail": "Invalid API key"}