diff --git a/.github/workflows/test-apps.yaml b/.github/workflows/test-apps.yaml new file mode 100644 index 0000000..4df28d2 --- /dev/null +++ b/.github/workflows/test-apps.yaml @@ -0,0 +1,18 @@ +name: Test All Apps + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test-all: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run tests script + run: ./scripts/test_apps.sh diff --git a/.gitignore b/.gitignore index 8ef54fe..c2cc155 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,4 @@ ml-models.ipr .DS_Store .databricks +.venv* diff --git a/dash-chatbot-app/__init__.py b/dash-chatbot-app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dash-chatbot-app/pytest.ini b/dash-chatbot-app/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/dash-chatbot-app/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/dash-chatbot-app/tests/test_model_serving_utils.py b/dash-chatbot-app/tests/test_model_serving_utils.py new file mode 100644 index 0000000..5c609e0 --- /dev/null +++ b/dash-chatbot-app/tests/test_model_serving_utils.py @@ -0,0 +1,24 @@ +from unittest.mock import patch +import sys +from model_serving_utils import query_endpoint + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_chat_format(mock_get_client): + # Arrange + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "choices": [{"message": {"role": "assistant", "content": "Hello there!"}}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Hello there!"} + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_agent_format(mock_get_client): + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "messages": [{"role": "tool", "content": "some tool call res"}, {"role": "assistant", "content": "Agent response."}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Agent response."} diff --git a/gradio-chatbot-app/pytest.ini b/gradio-chatbot-app/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/gradio-chatbot-app/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/gradio-chatbot-app/tests/test_model_serving_utils.py b/gradio-chatbot-app/tests/test_model_serving_utils.py new file mode 100644 index 0000000..5c609e0 --- /dev/null +++ b/gradio-chatbot-app/tests/test_model_serving_utils.py @@ -0,0 +1,24 @@ +from unittest.mock import patch +import sys +from model_serving_utils import query_endpoint + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_chat_format(mock_get_client): + # Arrange + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "choices": [{"message": {"role": "assistant", "content": "Hello there!"}}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Hello there!"} + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_agent_format(mock_get_client): + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "messages": [{"role": "tool", "content": "some tool call res"}, {"role": "assistant", "content": "Agent response."}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Agent response."} diff --git a/scripts/test_apps.sh b/scripts/test_apps.sh new file mode 100755 index 0000000..b938505 --- /dev/null +++ b/scripts/test_apps.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +GLOBAL_TEST_REQS="test-requirements.txt" + +UNTESTED_APPS=( + dash-data-app-obo-user + dash-data-app + dash-hello-world-app + flask-hello-world-app + gradio-data-app-obo-user + gradio-data-app + gradio-hello-world-app + shiny-data-app-obo-user + shiny-data-app + shiny-hello-world-app + streamlit-data-app-obo-user + streamlit-data-app + streamlit-hello-world-app + shiny-chatbot-app + gradio-chatbot-app + dash-chatbot-app +) + +is_untested() { + for app in "${UNTESTED_APPS[@]}"; do + [[ "$1" == "$app" ]] && return 0 + done + return 1 +} + +echo "๐Ÿ” Checking for test coverage..." + +missing_tests=() +for dir in */; do + base=$(basename "$dir") + + if is_untested "$base"; then + continue + fi + + if [[ -f "$dir/app.yaml" ]]; then + test_dir="${base}/tests" + if [[ ! -d "$test_dir" ]]; then + missing_tests+=("$base") + fi + fi +done + +if (( ${#missing_tests[@]} > 0 )); then + echo "โŒ The following apps have app.yaml but no tests:" + for app in "${missing_tests[@]}"; do + echo " - $app" + done + echo "Please add tests under tests// or add to UNTESTED_APPS." + exit 1 +fi + +echo "โœ… All testable apps have test coverage. Running tests..." + +for dir in */; do + base=$(basename "$dir") + + if is_untested "$base"; then + echo "๐ŸŸก Skipping $base (allowlisted as an untested app)" + continue + fi + + if [[ ! -f "$dir/app.yaml" ]]; then + echo "โš ๏ธ Skipping $base (no app.yaml)" + continue + fi + + echo "๐Ÿ”ง Testing $base..." + + VENV_DIR=".venv-${base}" + python3 -m venv "$VENV_DIR" + source "$VENV_DIR/bin/activate" + + pip install -U pip + pip install -r "$GLOBAL_TEST_REQS" + pip install -r "$dir/requirements.txt" + + test_dir="${base}/tests" + echo "๐Ÿงช Running tests in $test_dir from $dir" + pytest --cov="$dir" "$test_dir" --rootdir="$dir" + + deactivate + rm -rf "$VENV_DIR" +done diff --git a/shiny-chatbot-app/pytest.ini b/shiny-chatbot-app/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/shiny-chatbot-app/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/shiny-chatbot-app/tests/test_model_serving_utils.py b/shiny-chatbot-app/tests/test_model_serving_utils.py new file mode 100644 index 0000000..5c609e0 --- /dev/null +++ b/shiny-chatbot-app/tests/test_model_serving_utils.py @@ -0,0 +1,24 @@ +from unittest.mock import patch +import sys +from model_serving_utils import query_endpoint + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_chat_format(mock_get_client): + # Arrange + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "choices": [{"message": {"role": "assistant", "content": "Hello there!"}}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Hello there!"} + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_agent_format(mock_get_client): + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "messages": [{"role": "tool", "content": "some tool call res"}, {"role": "assistant", "content": "Agent response."}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Agent response."} diff --git a/streamlit-chatbot-app/pytest.ini b/streamlit-chatbot-app/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/streamlit-chatbot-app/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/streamlit-chatbot-app/tests/test_app.py b/streamlit-chatbot-app/tests/test_app.py new file mode 100644 index 0000000..9a3daf3 --- /dev/null +++ b/streamlit-chatbot-app/tests/test_app.py @@ -0,0 +1,18 @@ +import os +from unittest.mock import patch +import streamlit.testing.v1 as st_test +from pathlib import Path + +@patch("model_serving_utils.query_endpoint") +def test_app_renders_and_responds(mock_query): + mock_query.return_value = {"role": "assistant", "content": "Mock response"} + os.environ["SERVING_ENDPOINT"] = "dummy-endpoint" + # Initial render + test_dir = Path(__file__).resolve().parent + app_path = test_dir.parent / "app.py" + app = st_test.AppTest.from_file(str(app_path)) + app.run() + # Set chat input and rerender + app.chat_input[0].set_value("Hi there!") + app.run() + assert "Mock response" in set([md.value for md in app.markdown]) diff --git a/streamlit-chatbot-app/tests/test_model_serving_utils.py b/streamlit-chatbot-app/tests/test_model_serving_utils.py new file mode 100644 index 0000000..5c609e0 --- /dev/null +++ b/streamlit-chatbot-app/tests/test_model_serving_utils.py @@ -0,0 +1,24 @@ +from unittest.mock import patch +import sys +from model_serving_utils import query_endpoint + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_chat_format(mock_get_client): + # Arrange + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "choices": [{"message": {"role": "assistant", "content": "Hello there!"}}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Hello there!"} + +@patch("model_serving_utils.get_deploy_client") +def test_query_endpoint_agent_format(mock_get_client): + mock_client = mock_get_client.return_value + mock_client.predict.return_value = { + "messages": [{"role": "tool", "content": "some tool call res"}, {"role": "assistant", "content": "Agent response."}] + } + + result = query_endpoint("dummy-endpoint", [{"role": "user", "content": "Hi"}], 400) + assert result == {"role": "assistant", "content": "Agent response."} diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..a579d52 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +pytest>=8.3.2 +pytest-cov +dash[testing]==3.0.2