Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 35 additions & 41 deletions tests/aignostics/application/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,29 @@
import contextlib
import json
import platform
import random
import re
from collections.abc import Generator
from datetime import UTC, datetime, timedelta
from pathlib import Path
from time import sleep
from unittest.mock import MagicMock, patch

import pandas as pd
import pytest
from aignx.codegen.exceptions import ForbiddenException
from aignx.codegen.exceptions import NotFoundException as ApiNotFound
from aignx.codegen.models import (
ItemOutput,
ItemResultReadResponse,
ItemState,
ItemTerminationReason,
RunItemStatistics,
RunOutput,
RunReadResponse,
RunState,
RunTerminationReason,
)
from loguru import logger
from tenacity import Retrying, retry, stop_after_attempt, wait_exponential
from typer.testing import CliRunner
Expand Down Expand Up @@ -847,8 +862,6 @@ def test_cli_run_list_for_organization(runner: CliRunner) -> None:
@pytest.mark.unit
def test_cli_run_list_forbidden_with_organization(runner: CliRunner) -> None:
"""Check ForbiddenException with --for-organization shows org-specific access denied message."""
from aignx.codegen.exceptions import ForbiddenException

with patch.object(
ApplicationService, "application_runs", side_effect=ForbiddenException(status=403, reason="Forbidden")
):
Expand All @@ -862,8 +875,6 @@ def test_cli_run_list_forbidden_with_organization(runner: CliRunner) -> None:
@pytest.mark.unit
def test_cli_run_list_forbidden_without_organization(runner: CliRunner) -> None:
"""Check ForbiddenException without --for-organization shows generic access denied message."""
from aignx.codegen.exceptions import ForbiddenException

with patch.object(
ApplicationService, "application_runs", side_effect=ForbiddenException(status=403, reason="Forbidden")
):
Expand Down Expand Up @@ -897,18 +908,6 @@ def test_cli_run_describe_not_found(runner: CliRunner, record_property) -> None:
@pytest.mark.integration
def test_cli_run_describe_json_includes_items(runner: CliRunner) -> None:
"""Check run describe --format=json includes items in output."""
from aignx.codegen.models import (
ItemOutput,
ItemResultReadResponse,
ItemState,
ItemTerminationReason,
RunItemStatistics,
RunOutput,
RunReadResponse,
RunState,
RunTerminationReason,
)

mock_run_data = RunReadResponse(
run_id="test-run-id-123",
application_id="test-app",
Expand Down Expand Up @@ -1111,8 +1110,8 @@ def test_cli_run_execute(runner: CliRunner, tmp_path: Path, record_property) ->
results_dir = tmp_path / SPOT_1_FILENAME.replace(".tiff", "")
assert results_dir.is_dir(), f"Expected directory {results_dir} not found"
files_in_dir = list(results_dir.glob("*"))
assert len(files_in_dir) == 9, (
f"Expected 9 files in {results_dir}, but found {len(files_in_dir)}: {[f.name for f in files_in_dir]}"
assert len(files_in_dir) == 12, (
f"Expected 12 files in {results_dir}, but found {len(files_in_dir)}: {[f.name for f in files_in_dir]}"
Comment on lines +1113 to +1114
)
print(f"Found files in {results_dir}:")
for filename, expected_size, tolerance_percent in SPOT_1_EXPECTED_RESULT_FILES:
Expand All @@ -1133,6 +1132,23 @@ def test_cli_run_execute(runner: CliRunner, tmp_path: Path, record_property) ->
f"({min_size} to {max_size} bytes, ±{tolerance_percent}% of {expected_size})"
)

# Validate parquet <-> GeoJSON row count parity for the 3 paired outputs
parquet_geojson_pairs = [
("tissue_qc_parquet_polygons.parquet", "tissue_qc_geojson_polygons.json"),
("tissue_segmentation_parquet_polygons.parquet", "tissue_segmentation_geojson_polygons.json"),
("cell_classification_parquet_polygons.parquet", "cell_classification_geojson_polygons.json"),
]
for parquet_filename, geojson_filename in parquet_geojson_pairs:
parquet_path = results_dir / parquet_filename
geojson_path = results_dir / geojson_filename
parquet_row_count = len(pd.read_parquet(parquet_path))
with geojson_path.open() as f:
geojson_feature_count = len(json.load(f)["features"])
assert parquet_row_count == geojson_feature_count, (
f"Row count mismatch between {parquet_filename} ({parquet_row_count} rows) "
f"and {geojson_filename} ({geojson_feature_count} features)"
)

# Validate the execute command exited successfully
assert result.exit_code == 0

Expand Down Expand Up @@ -1222,9 +1238,6 @@ def test_cli_run_update_item_metadata_not_dict(runner: CliRunner) -> None:
@pytest.mark.sequential
def test_cli_run_dump_and_update_custom_metadata(runner: CliRunner, tmp_path: Path) -> None:
"""Test dumping and updating custom metadata via CLI commands."""
import json
import random

unique_tag = f"test_metadata_{datetime.now(tz=UTC).timestamp()}"
with submitted_run(runner, tmp_path, CSV_CONTENT_SPOT0, extra_args=["--tags", unique_tag, "--force"]) as run_id:
# Step 1: Dump initial custom metadata of run
Expand Down Expand Up @@ -1313,11 +1326,8 @@ def test_cli_run_dump_and_update_custom_metadata(runner: CliRunner, tmp_path: Pa
@pytest.mark.e2e
@pytest.mark.timeout(timeout=240)
@pytest.mark.sequential
def test_cli_run_dump_and_update_item_custom_metadata(runner: CliRunner, tmp_path: Path) -> None: # noqa: PLR0915
def test_cli_run_dump_and_update_item_custom_metadata(runner: CliRunner, tmp_path: Path) -> None:
"""Test dumping and updating item custom metadata via CLI commands."""
import json
import random

unique_tag = f"test_item_metadata_{datetime.now(tz=UTC).timestamp()}"
# CSV_CONTENT_SPOT0 uses SPOT_0_FILENAME as external_id, which the describe output surfaces
# as "Item External ID: `...`" — the get_external_id() helper below captures it dynamically.
Expand Down Expand Up @@ -1773,8 +1783,6 @@ def test_cli_application_version_document_describe_success(runner: CliRunner, re
def test_cli_application_version_document_describe_not_found(runner: CliRunner, record_property) -> None:
"""`application version document describe` exits 2 with a clear message on 404."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-03")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_documents = MagicMock()
fake_documents.details.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)
fake_client = MagicMock()
Expand Down Expand Up @@ -1870,8 +1878,6 @@ def test_cli_application_version_document_list_json_empty(runner: CliRunner, rec
def test_cli_application_version_document_list_resolve_not_found_text(runner: CliRunner, record_property) -> None:
"""`application version document list` exits 2 when the application version cannot be resolved."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-01")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_client = MagicMock()
fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)

Expand All @@ -1888,8 +1894,6 @@ def test_cli_application_version_document_list_resolve_not_found_text(runner: Cl
def test_cli_application_version_document_list_resolve_not_found_json(runner: CliRunner, record_property) -> None:
"""`application version document list --format json` emits structured error on 404."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-01")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_client = MagicMock()
fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)

Expand Down Expand Up @@ -1976,8 +1980,6 @@ def test_cli_application_version_document_describe_json_success(runner: CliRunne
def test_cli_application_version_document_describe_resolve_not_found_text(runner: CliRunner, record_property) -> None:
"""`describe` exits 2 when the application version cannot be resolved (text format)."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-03")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_client = MagicMock()
fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)

Expand All @@ -1996,8 +1998,6 @@ def test_cli_application_version_document_describe_resolve_not_found_text(runner
def test_cli_application_version_document_describe_resolve_not_found_json(runner: CliRunner, record_property) -> None:
"""`describe --format json` emits structured error when version cannot be resolved."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-03")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_client = MagicMock()
fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)

Expand Down Expand Up @@ -2026,8 +2026,6 @@ def test_cli_application_version_document_describe_resolve_not_found_json(runner
def test_cli_application_version_document_describe_not_found_json(runner: CliRunner, record_property) -> None:
"""`describe --format json` emits structured error when the document is missing."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-03")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_documents = MagicMock()
fake_documents.details.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)
fake_client = MagicMock()
Expand Down Expand Up @@ -2111,8 +2109,6 @@ def test_cli_application_version_document_download_resolve_not_found(
) -> None:
"""`download` exits 2 when the application version cannot be resolved."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-04")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_client = MagicMock()
fake_client.applications.versions.documents.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)

Expand Down Expand Up @@ -2142,8 +2138,6 @@ def test_cli_application_version_document_download_not_found(
) -> None:
"""`download` exits 2 with a clear message when the document does not exist."""
record_property("tested-item-id", "TC-APPLICATION-CLI-05-04")
from aignx.codegen.exceptions import NotFoundException as ApiNotFound

fake_documents = MagicMock()
fake_documents.download_to_path.side_effect = ApiNotFound(status=404, reason=API_REASON_NOT_FOUND)
fake_client = MagicMock()
Expand Down
25 changes: 22 additions & 3 deletions tests/aignostics/application/gui_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests to verify the GUI functionality of the application module."""

import contextlib
import json
import re
import tempfile
from asyncio import sleep, to_thread
Expand All @@ -9,6 +10,7 @@
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pandas as pd
import pytest
from nicegui.testing import User
from typer.testing import CliRunner
Expand Down Expand Up @@ -354,7 +356,7 @@ async def test_gui_download_dataset_via_application_to_run_cancel_to_find_back(
@pytest.mark.flaky(retries=1, delay=5)
@pytest.mark.timeout(timeout=60 * 10)
@pytest.mark.sequential # Helps on Linux with image analysis step otherwise timing out
async def test_gui_run_download( # noqa: PLR0915
async def test_gui_run_download( # noqa: PLR0914, PLR0915
user: User, runner: CliRunner, tmp_path: Path, silent_logging: None, record_property
) -> None:
"""Test that the user can download a run result via the GUI."""
Expand Down Expand Up @@ -440,8 +442,8 @@ async def test_gui_run_download( # noqa: PLR0915

# Check for files in the results directory
files_in_results_dir = list(results_dir.glob("*"))
assert len(files_in_results_dir) == 9, (
f"Expected 9 files in {results_dir}, but found {len(files_in_results_dir)}: "
assert len(files_in_results_dir) == 12, (
f"Expected 12 files in {results_dir}, but found {len(files_in_results_dir)}: "
Comment on lines +445 to +446
f"{[f.name for f in files_in_results_dir]}"
)

Expand All @@ -464,6 +466,23 @@ async def test_gui_run_download( # noqa: PLR0915
f"({min_size} to {max_size} bytes, ±{tolerance_percent}% of {expected_size})"
)

# Validate parquet <-> GeoJSON row count parity for the 3 paired outputs
parquet_geojson_pairs = [
("tissue_qc_parquet_polygons.parquet", "tissue_qc_geojson_polygons.json"),
("tissue_segmentation_parquet_polygons.parquet", "tissue_segmentation_geojson_polygons.json"),
("cell_classification_parquet_polygons.parquet", "cell_classification_geojson_polygons.json"),
]
for parquet_filename, geojson_filename in parquet_geojson_pairs:
parquet_path = results_dir / parquet_filename
geojson_path = results_dir / geojson_filename
parquet_row_count = len(pd.read_parquet(parquet_path))
with geojson_path.open() as f:
geojson_feature_count = len(json.load(f)["features"])
assert parquet_row_count == geojson_feature_count, (
f"Row count mismatch between {parquet_filename} ({parquet_row_count} rows) "
f"and {geojson_filename} ({geojson_feature_count} features)"
)


@pytest.mark.integration
@pytest.mark.sequential
Expand Down
48 changes: 30 additions & 18 deletions tests/constants_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,18 @@
# SPOT_0: uv run pytest tests/aignostics/application/gui_test.py::test_gui_run_download -s --no-cov
# SPOT_1: uv run pytest tests/aignostics/application/cli_test.py::test_cli_run_execute -s --no-cov
SPOT_0_EXPECTED_RESULT_FILES = [
("tissue_qc_segmentation_map_image.tiff", 1642856, 10),
("tissue_qc_geojson_polygons.json", 259955, 10),
("tissue_segmentation_geojson_polygons.json", 887003, 10),
("readout_generation_slide_readouts.csv", 303217, 10),
("readout_generation_cell_readouts.csv", 1658344, 10),
("cell_classification_geojson_polygons.json", 11218951, 10),
("tissue_segmentation_segmentation_map_image.tiff", 2945078, 10),
("tissue_segmentation_csv_class_information.csv", 452, 10),
("tissue_qc_csv_class_information.csv", 285, 10),
("tissue_qc_segmentation_map_image.tiff", 470150, 10),
("tissue_qc_geojson_polygons.json", 171251, 10),
("tissue_segmentation_geojson_polygons.json", 185516, 10),
("readout_generation_slide_readouts.csv", 300205, 10),
("readout_generation_cell_readouts.csv", 2417117, 10),
("cell_classification_geojson_polygons.json", 16673412, 10),
("tissue_segmentation_segmentation_map_image.tiff", 527264, 10),
("tissue_segmentation_csv_class_information.csv", 443, 10),
("tissue_qc_csv_class_information.csv", 286, 10),
("tissue_qc_parquet_polygons.parquet", 34346, 10),
("tissue_segmentation_parquet_polygons.parquet", 39185, 10),
("cell_classification_parquet_polygons.parquet", 5476364, 10),
Comment on lines 85 to +97
]
SPOT_0_EXPECTED_CELLS_CLASSIFIED = (39798, 10)

Expand All @@ -105,6 +108,9 @@
("tissue_segmentation_segmentation_map_image.tiff", 1783952, 10),
("tissue_segmentation_csv_class_information.csv", 446, 10),
("tissue_qc_csv_class_information.csv", 290, 10),
("tissue_qc_parquet_polygons.parquet", 29049, 10),
("tissue_segmentation_parquet_polygons.parquet", 56682, 10),
("cell_classification_parquet_polygons.parquet", 838533, 10),
]

case "staging":
Expand All @@ -124,15 +130,18 @@

# See production block above for instructions on how to update these sizes.
SPOT_0_EXPECTED_RESULT_FILES = [
("tissue_qc_segmentation_map_image.tiff", 1642856, 10),
("tissue_qc_geojson_polygons.json", 259955, 10),
("tissue_segmentation_geojson_polygons.json", 887003, 10),
("readout_generation_slide_readouts.csv", 303217, 10),
("readout_generation_cell_readouts.csv", 1658344, 10),
("cell_classification_geojson_polygons.json", 11218951, 10),
("tissue_segmentation_segmentation_map_image.tiff", 2945078, 10),
("tissue_segmentation_csv_class_information.csv", 452, 10),
("tissue_qc_csv_class_information.csv", 285, 10),
("tissue_qc_segmentation_map_image.tiff", 470150, 10),
("tissue_qc_geojson_polygons.json", 171251, 10),
("tissue_segmentation_geojson_polygons.json", 185516, 10),
("readout_generation_slide_readouts.csv", 300205, 10),
("readout_generation_cell_readouts.csv", 2417117, 10),
("cell_classification_geojson_polygons.json", 16673412, 10),
("tissue_segmentation_segmentation_map_image.tiff", 527264, 10),
("tissue_segmentation_csv_class_information.csv", 443, 10),
("tissue_qc_csv_class_information.csv", 286, 10),
("tissue_qc_parquet_polygons.parquet", 34346, 10),
("tissue_segmentation_parquet_polygons.parquet", 39185, 10),
("cell_classification_parquet_polygons.parquet", 5476364, 10),
]
SPOT_0_EXPECTED_CELLS_CLASSIFIED = (39798, 10)

Expand All @@ -146,6 +155,9 @@
("tissue_segmentation_segmentation_map_image.tiff", 1783952, 10),
("tissue_segmentation_csv_class_information.csv", 446, 10),
("tissue_qc_csv_class_information.csv", 290, 10),
("tissue_qc_parquet_polygons.parquet", 29049, 10),
("tissue_segmentation_parquet_polygons.parquet", 56682, 10),
("cell_classification_parquet_polygons.parquet", 838533, 10),
]

case _:
Expand Down