Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7e8b43b
feat: openCV resizing
Feb 11, 2026
dde2da1
fix: linting and enum for selecting openCV resampling algos
Feb 11, 2026
13c490c
chore: remove irrelevant comment
Feb 12, 2026
8936a6e
refactor: imagemagick is no longer a dependency for the functional tests
Feb 12, 2026
0540544
feat: entirely remove PIL from this project
Feb 12, 2026
0452ebd
chore: unify image_generation files into once place
Feb 12, 2026
a67d553
chore: remove defunct file
Feb 12, 2026
81dd1b1
style: lint
Feb 12, 2026
703090f
style: standardise opencv2 imports to be import cv2 as cv
Feb 12, 2026
f040807
doc: remove reference to PIL in docs
Feb 12, 2026
a6634af
chore: capitalisation
anna-singleton-resolver Feb 12, 2026
6e1e1ca
chore: grammar
anna-singleton-resolver Feb 12, 2026
a95a7c5
refactor: split out image_generation file into separate module
Feb 12, 2026
d7921ca
feature: add functional end to end tests
Oct 10, 2025
6338189
fix: fix ruff linting
Oct 10, 2025
d0327e2
fix: fix ruff format check
Oct 10, 2025
fae867f
fix: fix type checks
Oct 10, 2025
71df79c
feat: add test cases for live model
Nov 25, 2025
ec83696
test: Improve e2e functional tests for classify_single
Dec 1, 2025
f569486
chore: Add VSCode workspace settings for Athena tests
Dec 3, 2025
1167de4
more configuration options for the client (#92)
anna-singleton-resolver Feb 11, 2026
45f00dc
test: add color channel functional test
Feb 12, 2026
781dec8
fix: streaming tests hang - add explicit aclose() to terminate gRPC s…
Feb 12, 2026
2e5206a
fix: close stream on error to prevent hanging
Feb 12, 2026
33c4aea
Fix functional tests: session-scoped auth, skip hanging test, fix col…
Feb 12, 2026
47531fc
test: reduce default streaming test image count to 50
Feb 12, 2026
5e91045
test: add integrator sample test set with 10 safe images
Feb 12, 2026
1918ac9
refactor: move e2e testcases to athena-protobufs for shared use
Feb 13, 2026
aed699b
chore: update athena-protobufs with testcases documentation
Feb 13, 2026
45e9976
chore: update athena-protobufs with Pexels license correction
Feb 13, 2026
ca751cf
chore: update athena-protobufs with safety clarifications
Feb 13, 2026
1321f3a
chore: update athena-protobufs with filename fix
Feb 13, 2026
b3aea37
fix: remove missing lakeside images and clean up exclusions list
Feb 13, 2026
d5f862a
chore: update athena-protobufs with testcases release workflow
Feb 13, 2026
6db0fc0
chore: update athena-protobufs with workflow fix
Feb 13, 2026
ca23957
Merge branch 'main' into feature/e2e-func-tests
iwillspeak Feb 13, 2026
b0ece7c
fix: resolve E501 line-too-long linting errors
Feb 13, 2026
fc8f9c1
chore: ruff
Feb 13, 2026
294df0e
fix: remove invalid aclose() calls on AsyncIterator
Feb 13, 2026
df885b6
refactor: clean up EXCLUDED_FILENAMES constant by removing obsolete c…
Feb 13, 2026
ab62517
test: Remove Redundant Assertions.
iwillspeak Feb 13, 2026
4bedcac
refactor: clean up EXCLUDED_FILENAMES constant by removing obsolete c…
Feb 13, 2026
823c3db
Apply suggestions from code review
iwillspeak Feb 13, 2026
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 .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
2 changes: 1 addition & 1 deletion athena-protobufs
Submodule athena-protobufs updated 117 files
2 changes: 1 addition & 1 deletion examples/classify_single_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ async def main() -> int:
resize_images=True,
compress_images=True,
timeout=30.0, # Shorter timeout for single requests
affiliate="Crisp",
affiliate=os.getenv("ATHENA_AFFILIATE", "athena-test"),
deployment_id="single-example-deployment", # Not used
)

Expand Down
1 change: 0 additions & 1 deletion examples/utils/streaming_classify_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ async def classify_images_break_on_first_result(
)

error_count = process_errors(logger, result, error_count)

break

except Exception:
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _create_base_test_image_opencv(width: int, height: int) -> np.ndarray:
]


@pytest_asyncio.fixture
@pytest_asyncio.fixture(scope="session")
async def credential_helper() -> CredentialHelper:
_ = load_dotenv()
client_id = os.environ["OAUTH_CLIENT_ID"]
Expand Down
Empty file.
70 changes: 70 additions & 0 deletions tests/functional/e2e/test_classify_single.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from pathlib import Path

import pytest

from resolver_athena_client.client.athena_client import AthenaClient
from resolver_athena_client.client.athena_options import AthenaOptions
from resolver_athena_client.client.channel import (
CredentialHelper,
create_channel_with_credentials,
)
from resolver_athena_client.client.models import ImageData
from tests.functional.e2e.testcases.parser import (
AthenaTestCase,
load_test_cases,
)

TEST_CASES = load_test_cases("integrator_sample")

FP_ERROR_TOLERANCE = 1e-4


@pytest.mark.asyncio
@pytest.mark.functional
@pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda tc: tc.id)
async def test_classify_single(
athena_options: AthenaOptions,
credential_helper: CredentialHelper,
test_case: AthenaTestCase,
) -> None:
"""Functional test for ClassifySingle endpoint and API methods.

This test creates a unique test image for each iteration and classifies it.

"""

# Create gRPC channel with credentials
channel = await create_channel_with_credentials(
athena_options.host, credential_helper
)
with Path.open(Path(test_case.filepath), "rb") as f:
image_bytes = f.read()

async with AthenaClient(channel, athena_options) as client:
image_data = ImageData(image_bytes)

# Classify with auto-generated correlation ID
result = await client.classify_single(image_data)

if result.error.code:
msg = f"Image Result Error: {result.error.message}"
pytest.fail(msg)

actual_output = {c.label: c.weight for c in result.classifications}
assert set(test_case.expected_output.keys()).issubset(
set(actual_output.keys())
), (
"Expected output to contain labels: ",
f"{test_case.expected_output.keys() - actual_output.keys()}",
)
actual_output = {k: actual_output[k] for k in test_case.expected_output}

for label in test_case.expected_output:
expected = test_case.expected_output[label]
actual = actual_output[label]
diff = abs(expected - actual)
assert diff < FP_ERROR_TOLERANCE, (
f"Weight for label '{label}' differs by more than "
f"{FP_ERROR_TOLERANCE}: expected={expected}, actual={actual}, "
f"diff={diff}"
)
Empty file.
38 changes: 38 additions & 0 deletions tests/functional/e2e/testcases/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
from pathlib import Path

# Path to the shared testcases directory in athena-protobufs
_REPO_ROOT = Path(__file__).parent.parent.parent.parent.parent
TESTCASES_DIR = _REPO_ROOT / "athena-protobufs" / "testcases"


class AthenaTestCase:
def __init__(
self,
filepath: str,
expected_output: list[float],
classification_labels: list[str],
) -> None:
self.id: str = "/".join(
Path(filepath).parts[-2:]
) # e.g. "ducks/duck1.jpg"
self.filepath: str = filepath
self.expected_output: dict[str, float] = dict(
zip(classification_labels, expected_output, strict=True)
)
self.classification_labels: list[str] = classification_labels


def load_test_cases(dirname: str = "benign_model") -> list[AthenaTestCase]:
with Path.open(
Path(TESTCASES_DIR / dirname / "expected_outputs.json"),
) as f:
test_cases = json.load(f)
return [
AthenaTestCase(
str(Path(TESTCASES_DIR / dirname / "images" / item[0])),
item[1],
test_cases["classification_labels"],
)
for item in test_cases["images"]
]
5 changes: 4 additions & 1 deletion tests/functional/test_classify_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def test_streaming_classify(
logger = logging.getLogger(__name__)

# Configuration
max_test_images = int(os.getenv("TEST_IMAGE_COUNT", str(5_000)))
max_test_images = int(os.getenv("TEST_IMAGE_COUNT", str(50)))
min_interval_ms = os.getenv("TEST_MIN_INTERVAL_MS", None)
if min_interval_ms is not None:
min_interval_ms = int(min_interval_ms)
Expand All @@ -59,6 +59,9 @@ async def test_streaming_classify(
assert sent == received, f"Incomplete: {sent} sent, {received} received"


@pytest.mark.skip(
reason="Relies on server-side shared queue behavior - needs investigation"
)
@pytest.mark.asyncio
@pytest.mark.functional
async def test_streaming_classify_with_reopened_stream(
Expand Down
110 changes: 110 additions & 0 deletions tests/functional/test_color_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Functional test for classifying images with specific color channels."""

import numpy as np
import pytest

from resolver_athena_client.client.athena_client import AthenaClient
from resolver_athena_client.client.athena_options import AthenaOptions
from resolver_athena_client.client.channel import (
CredentialHelper,
create_channel_with_credentials,
)
from resolver_athena_client.client.consts import EXPECTED_HEIGHT, EXPECTED_WIDTH
from resolver_athena_client.client.models import ImageData


def create_color_channel_image(
channel: str, width: int = EXPECTED_WIDTH, height: int = EXPECTED_HEIGHT
) -> bytes:
"""Create a raw BGR image with only one channel set to 255.

Args:
channel: Color channel to set - 'red', 'green', or 'blue'
width: Image width in pixels
height: Image height in pixels

Returns:
Raw BGR uint8 image bytes

"""
# Create BGR image (3 channels)
img = np.zeros((height, width, 3), dtype=np.uint8)

# Set the specified channel to 255
if channel == "red":
img[:, :, 2] = 255 # Red is channel 2 in BGR
elif channel == "green":
img[:, :, 1] = 255 # Green is channel 1 in BGR
elif channel == "blue":
img[:, :, 0] = 255 # Blue is channel 0 in BGR
else:
msg = f"Invalid channel: {channel}. Must be 'red', 'green', or 'blue'"
raise ValueError(msg)

return img.tobytes()


@pytest.mark.asyncio
@pytest.mark.functional
async def test_classify_color_channels(
athena_options: AthenaOptions, credential_helper: CredentialHelper
) -> None:
"""Test classification of three images with distinct color channels.

Creates and classifies three 448x448x3 images:
- Red image: R=255, G=0, B=0
- Green image: R=0, G=255, B=0
- Blue image: R=0, G=0, B=255
"""
# Create gRPC channel with credentials
channel = await create_channel_with_credentials(
athena_options.host, credential_helper
)

async with AthenaClient(channel, athena_options) as client:
# Test red channel image
red_image_bytes = create_color_channel_image("red")
red_image_data = ImageData(red_image_bytes)

red_result = await client.classify_single(red_image_data)

if red_result.error.code:
msg = f"Red image classification error: {red_result.error.message}"
pytest.fail(msg)

assert len(red_result.classifications) > 0, (
"No classifications for red image"
)

# Test green channel image
green_image_bytes = create_color_channel_image("green")
green_image_data = ImageData(green_image_bytes)

green_result = await client.classify_single(green_image_data)

if green_result.error.code:
msg = (
"Green image classification error: "
f"{green_result.error.message}"
)
pytest.fail(msg)

assert len(green_result.classifications) > 0, (
"No classifications for green image"
)

# Test blue channel image
blue_image_bytes = create_color_channel_image("blue")
blue_image_data = ImageData(blue_image_bytes)

blue_result = await client.classify_single(blue_image_data)

if blue_result.error.code:
msg = (
f"Blue image classification error: {blue_result.error.message}"
)
pytest.fail(msg)

assert len(blue_result.classifications) > 0, (
"No classifications for blue image"
)
1 change: 0 additions & 1 deletion tests/utils/streaming_classify_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ async def classify_images_break_on_first_result(
)

error_count = process_errors(logger, result, error_count)

break

except Exception:
Expand Down