Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
71 changes: 28 additions & 43 deletions tests/aignostics/application/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import contextlib
import json
import platform
import random
import re
from collections.abc import Generator
from datetime import UTC, datetime, timedelta
Expand All @@ -11,6 +12,19 @@
from unittest.mock import MagicMock, patch

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 All @@ -19,7 +33,7 @@
from aignostics.cli import cli
from aignostics.platform import LIST_APPLICATION_RUNS_MAX_PAGE_SIZE
from aignostics.utils import Health, sanitize_path
from tests.conftest import normalize_output, print_directory_structure
from tests.conftest import assert_parquet_geojson_parity, normalize_output, print_directory_structure
from tests.constants_test import (
HETA_APPLICATION_ID,
HETA_APPLICATION_VERSION,
Expand Down Expand Up @@ -224,7 +238,11 @@ def test_cli_application_dump_schemata(runner: CliRunner, tmp_path: Path, record
],
)
assert result.exit_code == 0
assert "Zipped 11 files" in normalize_output(result.output)
output = normalize_output(result.output)
zipped_match = re.search(r"Zipped (\d+) files", output)
assert zipped_match is not None, f"Expected 'Zipped N files' in output: {output}"
zipped_count = int(zipped_match.group(1))
assert 12 <= zipped_count <= 16, f"Expected between 12 and 16 schemata files, got {zipped_count}"
zip_file = sanitize_path(Path(tmp_path / f"{HETA_APPLICATION_ID}_{HETA_APPLICATION_VERSION}_schemata.zip"))
assert zip_file.exists(), f"Expected zip file {zip_file} not found"

Expand Down Expand Up @@ -847,8 +865,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 +878,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 +911,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 +1113,10 @@ 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]}"
expected_count = len(SPOT_1_EXPECTED_RESULT_FILES)
assert len(files_in_dir) == expected_count, (
f"Expected {expected_count} files in {results_dir}, but found {len(files_in_dir)}: "
f"{[f.name for f in files_in_dir]}"
)
print(f"Found files in {results_dir}:")
for filename, expected_size, tolerance_percent in SPOT_1_EXPECTED_RESULT_FILES:
Expand All @@ -1133,6 +1137,9 @@ 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 parity: area for segmentation, count for cell classification.
assert_parquet_geojson_parity(results_dir)

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

Expand Down Expand Up @@ -1222,9 +1229,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 +1317,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 +1774,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 +1869,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 +1885,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 +1971,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 +1989,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 +2017,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 +2100,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 +2129,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
23 changes: 15 additions & 8 deletions tests/aignostics/application/gui_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
_resolve_artifact_url_or_notify,
)
from aignostics.cli import cli
from tests.conftest import assert_notified, normalize_output, print_directory_structure
from tests.conftest import assert_notified, assert_parquet_geojson_parity, normalize_output, print_directory_structure
from tests.constants_test import (
HETA_APPLICATION_ID,
HETA_APPLICATION_VERSION,
Expand All @@ -31,6 +31,7 @@
SPOT_0_FILESIZE,
SPOT_0_GS_URL,
SPOT_1_FILENAME,
SPOT_1_FILESIZE,
SPOT_1_GS_URL,
)

Expand Down Expand Up @@ -214,7 +215,7 @@ async def test_gui_download_dataset_via_application_to_run_cancel_to_find_back(
assert SPOT_1_FILENAME in normalize_output(result.stdout)
expected_file = Path(tmp_path) / SPOT_1_FILENAME
assert expected_file.exists(), f"Expected file {expected_file} not found"
assert expected_file.stat().st_size == 14681750
assert expected_file.stat().st_size == SPOT_1_FILESIZE

# Open the GUI and navigate to Atlas H&E-TME application
await user.open("/")
Expand Down Expand Up @@ -354,7 +355,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 +441,9 @@ 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)}: "
expected_count = len(SPOT_0_EXPECTED_RESULT_FILES)
assert len(files_in_results_dir) == expected_count, (
f"Expected {expected_count} files in {results_dir}, but found {len(files_in_results_dir)}: "
f"{[f.name for f in files_in_results_dir]}"
)

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

# Validate parquet <-> GeoJSON parity: area for segmentation, count for cell classification.
assert_parquet_geojson_parity(results_dir)


@pytest.mark.integration
@pytest.mark.sequential
Expand All @@ -479,12 +484,14 @@ async def test_gui_run_results_pagination_show_more_button_hidden_when_few_resul
"""
record_property("tested-item-id", "SPEC-APPLICATION-SERVICE, SPEC-GUI-SERVICE")

# Find a run with fewer items than RESULTS_PAGE_SIZE
# Find a run with fewer items than RESULTS_PAGE_SIZE.
# Omit has_output so the server-side filter is applied without client-side pagination:
# item_count already acts as a proxy (runs with no output show item_count=0 and fail
# the 0 < item_count <= RESULTS_PAGE_SIZE check below).
runs = Service().application_runs(
Comment on lines +487 to 491
Comment on lines +487 to 491
application_id=HETA_APPLICATION_ID,
application_version=HETA_APPLICATION_VERSION,
has_output=True,
limit=20,
limit=5,
)

# Find a run with few enough items
Expand Down
4 changes: 2 additions & 2 deletions tests/aignostics/dataset/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from aignostics.cli import cli
from tests.conftest import normalize_output
from tests.constants_test import SPOT_1_FILENAME, SPOT_1_GS_URL
from tests.constants_test import SPOT_1_FILENAME, SPOT_1_FILESIZE, SPOT_1_GS_URL

SERIES_UID = "1.3.6.1.4.1.5962.99.1.1069745200.1645485340.1637452317744.2.0"
THUMBNAIL_UID = "1.3.6.1.4.1.5962.99.1.1038911754.1238045814.1637421484298.15.0"
Expand Down Expand Up @@ -149,7 +149,7 @@ def test_cli_aignostics_download_sample(runner: CliRunner, tmp_path: Path, recor

expected_file = tmp_path / SPOT_1_FILENAME
assert expected_file.exists(), f"Expected file {expected_file} not found"
assert expected_file.stat().st_size == 14681750
assert expected_file.stat().st_size == SPOT_1_FILESIZE


@pytest.mark.integration
Expand Down
Loading
Loading