Skip to content

Commit ec35916

Browse files
stewart-ranna-singleton-resolveranna-singleton-resolverCopilotRobertson
authored
Adds end to end functional tests (#45)
* feat: openCV resizing * fix: linting and enum for selecting openCV resampling algos * chore: remove irrelevant comment * refactor: imagemagick is no longer a dependency for the functional tests * feat: entirely remove PIL from this project * chore: unify image_generation files into once place * chore: remove defunct file * style: lint * style: standardise opencv2 imports to be import cv2 as cv * doc: remove reference to PIL in docs * chore: capitalisation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: grammar Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: split out image_generation file into separate module * feature: add functional end to end tests * fix: fix ruff linting * fix: fix ruff format check * fix: fix type checks * feat: add test cases for live model * test: Improve e2e functional tests for classify_single Better Logging, Enhanced Error Handling, and Updated Expected Outputs * chore: Add VSCode workspace settings for Athena tests * more configuration options for the client (#92) * feat: correlationid can be specified on ImageData * feat: set brotli compression quality * style: lint * fix: fixup example code * feat: configurable resizing algorithm --------- Co-authored-by: anna-singleton-resolver <anna.singleton@resolver.com> * test: add color channel functional test * fix: streaming tests hang - add explicit aclose() to terminate gRPC stream - Call results.aclose() before breaking from response loop to properly terminate the async generator and underlying gRPC stream - Update snapshot for lakeside/pexels-pixabay-210288.jpg (weight 0.9259 -> 0.9215) - Apply same fix to examples/utils for consistency * fix: close stream on error to prevent hanging Add aclose() in exception handlers to ensure the gRPC stream is terminated even when an error occurs during classification. * Fix functional tests: session-scoped auth, skip hanging test, fix color channels * test: reduce default streaming test image count to 50 * test: add integrator sample test set with 10 safe images * refactor: move e2e testcases to athena-protobufs for shared use - Move testcases/ to athena-protobufs submodule - Add .gitattributes for LFS tracking in protobufs - Update parser.py to load from shared location - Remove local testcases (now in submodule) - Fix stale PIL import from rebase conflict * chore: update athena-protobufs with testcases documentation * chore: update athena-protobufs with Pexels license correction * chore: update athena-protobufs with safety clarifications * chore: update athena-protobufs with filename fix * fix: remove missing lakeside images and clean up exclusions list * chore: update athena-protobufs with testcases release workflow * chore: update athena-protobufs with workflow fix * fix: resolve E501 line-too-long linting errors * chore: ruff * fix: remove invalid aclose() calls on AsyncIterator * refactor: clean up EXCLUDED_FILENAMES constant by removing obsolete comment * test: Remove Redundant Assertions. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: clean up EXCLUDED_FILENAMES constant by removing obsolete comment --------- Co-authored-by: anna-singleton-resolver <anna.singleton@resolver.com> Co-authored-by: anna <anna.singleton@crispthinking.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Robertson <stewart.robertson@kroll.com> Co-authored-by: Will Speak <will.speak@kroll.com> Co-authored-by: Will Speak <lithiumflame@gmail.com>
1 parent cf5c312 commit ec35916

12 files changed

Lines changed: 232 additions & 6 deletions

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"python.testing.pytestArgs": [
3+
"tests"
4+
],
5+
"python.testing.unittestEnabled": false,
6+
"python.testing.pytestEnabled": true
7+
}

athena-protobufs

Submodule athena-protobufs updated 117 files

examples/classify_single_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ async def main() -> int:
228228
resize_images=True,
229229
compress_images=True,
230230
timeout=30.0, # Shorter timeout for single requests
231-
affiliate="Crisp",
231+
affiliate=os.getenv("ATHENA_AFFILIATE", "athena-test"),
232232
deployment_id="single-example-deployment", # Not used
233233
)
234234

examples/utils/streaming_classify_utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ async def classify_images_break_on_first_result(
217217
)
218218

219219
error_count = process_errors(logger, result, error_count)
220-
221220
break
222221

223222
except Exception:

tests/functional/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def _create_base_test_image_opencv(width: int, height: int) -> np.ndarray:
6060
]
6161

6262

63-
@pytest_asyncio.fixture
63+
@pytest_asyncio.fixture(scope="session")
6464
async def credential_helper() -> CredentialHelper:
6565
_ = load_dotenv()
6666
client_id = os.environ["OAUTH_CLIENT_ID"]

tests/functional/e2e/__init__.py

Whitespace-only changes.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from resolver_athena_client.client.athena_client import AthenaClient
6+
from resolver_athena_client.client.athena_options import AthenaOptions
7+
from resolver_athena_client.client.channel import (
8+
CredentialHelper,
9+
create_channel_with_credentials,
10+
)
11+
from resolver_athena_client.client.models import ImageData
12+
from tests.functional.e2e.testcases.parser import (
13+
AthenaTestCase,
14+
load_test_cases,
15+
)
16+
17+
TEST_CASES = load_test_cases("integrator_sample")
18+
19+
FP_ERROR_TOLERANCE = 1e-4
20+
21+
22+
@pytest.mark.asyncio
23+
@pytest.mark.functional
24+
@pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda tc: tc.id)
25+
async def test_classify_single(
26+
athena_options: AthenaOptions,
27+
credential_helper: CredentialHelper,
28+
test_case: AthenaTestCase,
29+
) -> None:
30+
"""Functional test for ClassifySingle endpoint and API methods.
31+
32+
This test creates a unique test image for each iteration and classifies it.
33+
34+
"""
35+
36+
# Create gRPC channel with credentials
37+
channel = await create_channel_with_credentials(
38+
athena_options.host, credential_helper
39+
)
40+
with Path.open(Path(test_case.filepath), "rb") as f:
41+
image_bytes = f.read()
42+
43+
async with AthenaClient(channel, athena_options) as client:
44+
image_data = ImageData(image_bytes)
45+
46+
# Classify with auto-generated correlation ID
47+
result = await client.classify_single(image_data)
48+
49+
if result.error.code:
50+
msg = f"Image Result Error: {result.error.message}"
51+
pytest.fail(msg)
52+
53+
actual_output = {c.label: c.weight for c in result.classifications}
54+
assert set(test_case.expected_output.keys()).issubset(
55+
set(actual_output.keys())
56+
), (
57+
"Expected output to contain labels: ",
58+
f"{test_case.expected_output.keys() - actual_output.keys()}",
59+
)
60+
actual_output = {k: actual_output[k] for k in test_case.expected_output}
61+
62+
for label in test_case.expected_output:
63+
expected = test_case.expected_output[label]
64+
actual = actual_output[label]
65+
diff = abs(expected - actual)
66+
assert diff < FP_ERROR_TOLERANCE, (
67+
f"Weight for label '{label}' differs by more than "
68+
f"{FP_ERROR_TOLERANCE}: expected={expected}, actual={actual}, "
69+
f"diff={diff}"
70+
)

tests/functional/e2e/testcases/__init__.py

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import json
2+
from pathlib import Path
3+
4+
# Path to the shared testcases directory in athena-protobufs
5+
_REPO_ROOT = Path(__file__).parent.parent.parent.parent.parent
6+
TESTCASES_DIR = _REPO_ROOT / "athena-protobufs" / "testcases"
7+
8+
9+
class AthenaTestCase:
10+
def __init__(
11+
self,
12+
filepath: str,
13+
expected_output: list[float],
14+
classification_labels: list[str],
15+
) -> None:
16+
self.id: str = "/".join(
17+
Path(filepath).parts[-2:]
18+
) # e.g. "ducks/duck1.jpg"
19+
self.filepath: str = filepath
20+
self.expected_output: dict[str, float] = dict(
21+
zip(classification_labels, expected_output, strict=True)
22+
)
23+
self.classification_labels: list[str] = classification_labels
24+
25+
26+
def load_test_cases(dirname: str = "benign_model") -> list[AthenaTestCase]:
27+
with Path.open(
28+
Path(TESTCASES_DIR / dirname / "expected_outputs.json"),
29+
) as f:
30+
test_cases = json.load(f)
31+
return [
32+
AthenaTestCase(
33+
str(Path(TESTCASES_DIR / dirname / "images" / item[0])),
34+
item[1],
35+
test_cases["classification_labels"],
36+
)
37+
for item in test_cases["images"]
38+
]

tests/functional/test_classify_streaming.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async def test_streaming_classify(
3838
logger = logging.getLogger(__name__)
3939

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

6161

62+
@pytest.mark.skip(
63+
reason="Relies on server-side shared queue behavior - needs investigation"
64+
)
6265
@pytest.mark.asyncio
6366
@pytest.mark.functional
6467
async def test_streaming_classify_with_reopened_stream(

0 commit comments

Comments
 (0)