143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.core.config import Settings
|
|
from app.core.errors import AppError
|
|
from app.models import AnalysisRun, Dataset, Job, Project
|
|
from app.services.detection_service import DetectionService
|
|
from app.services.job_service import JobService
|
|
from app.services.segmentation_service import SegmentationService
|
|
|
|
|
|
class FakeSession:
|
|
"""Minimal session double without rollback support, mirroring existing test doubles."""
|
|
|
|
def __init__(self, objects=None) -> None:
|
|
self.objects = objects or {}
|
|
self.added = []
|
|
self.commits = 0
|
|
|
|
def get(self, model, item_id):
|
|
return self.objects.get((model, item_id))
|
|
|
|
def add(self, item) -> None:
|
|
self.added.append(item)
|
|
if getattr(item, "id", None) is not None:
|
|
self.objects[(item.__class__, item.id)] = item
|
|
|
|
def commit(self) -> None:
|
|
self.commits += 1
|
|
|
|
def refresh(self, item) -> None:
|
|
pass
|
|
|
|
|
|
def _project_and_dataset():
|
|
project_id = uuid4()
|
|
dataset_id = uuid4()
|
|
project = Project(id=project_id, name="Mol")
|
|
dataset = Dataset(
|
|
id=dataset_id,
|
|
project_id=project_id,
|
|
name="ortho.tif",
|
|
dataset_type="raster",
|
|
source="user_upload",
|
|
storage_path="storage/uploads/ortho.tif",
|
|
)
|
|
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
|
|
return db, project_id, dataset_id
|
|
|
|
|
|
def _statuses(db: FakeSession) -> tuple[list[str], list[str]]:
|
|
runs = [item.status for item in db.added if isinstance(item, AnalysisRun)]
|
|
jobs = [item.status for item in db.added if isinstance(item, Job)]
|
|
return runs, jobs
|
|
|
|
|
|
def test_invalid_fixture_detections_mark_run_and_job_failed() -> None:
|
|
db, project_id, dataset_id = _project_and_dataset()
|
|
|
|
with pytest.raises(AppError) as exc_info:
|
|
DetectionService.run_detection(
|
|
db=db,
|
|
project_id=project_id,
|
|
dataset_id=dataset_id,
|
|
model_id="manual-fixture-detector",
|
|
confidence_threshold=0.5,
|
|
parameters_json={"fixture_mode": True, "fixture_detections": "not-a-list"},
|
|
settings=Settings(_env_file=None),
|
|
)
|
|
|
|
assert exc_info.value.code == "INVALID_FIXTURE_DETECTIONS"
|
|
run_statuses, job_statuses = _statuses(db)
|
|
assert run_statuses and all(status == "failed" for status in run_statuses)
|
|
assert job_statuses and all(status == "failed" for status in job_statuses)
|
|
|
|
|
|
def test_invalid_fixture_segmentations_mark_run_and_job_failed() -> None:
|
|
db, project_id, dataset_id = _project_and_dataset()
|
|
|
|
with pytest.raises(AppError) as exc_info:
|
|
SegmentationService.run_segmentation(
|
|
db=db,
|
|
project_id=project_id,
|
|
dataset_id=dataset_id,
|
|
model_id="fixture-segmenter",
|
|
confidence_threshold=0.5,
|
|
parameters_json={"fixture_mode": True, "fixture_segmentations": "not-a-list"},
|
|
settings=Settings(_env_file=None),
|
|
)
|
|
|
|
assert exc_info.value.code == "INVALID_FIXTURE_SEGMENTATIONS"
|
|
run_statuses, job_statuses = _statuses(db)
|
|
assert run_statuses and all(status == "failed" for status in run_statuses)
|
|
assert job_statuses and all(status == "failed" for status in job_statuses)
|
|
|
|
|
|
def test_unexpected_error_in_sync_job_marks_job_failed() -> None:
|
|
project_id = uuid4()
|
|
db = FakeSession(objects={(Project, project_id): Project(id=project_id, name="Mol")})
|
|
|
|
def exploding_operation():
|
|
raise RuntimeError("unexpected internal failure")
|
|
|
|
with pytest.raises(RuntimeError):
|
|
JobService.run_sync_job(
|
|
db=db,
|
|
project_id=project_id,
|
|
job_type="test.unexpected",
|
|
parameters={},
|
|
operation=exploding_operation,
|
|
)
|
|
|
|
jobs = [item for item in db.added if isinstance(item, Job)]
|
|
assert jobs
|
|
final_job = jobs[-1]
|
|
assert final_job.status == "failed"
|
|
assert "Unexpected internal error" in (final_job.error_message or "")
|
|
|
|
|
|
def test_app_error_in_sync_job_still_marks_job_failed() -> None:
|
|
project_id = uuid4()
|
|
db = FakeSession(objects={(Project, project_id): Project(id=project_id, name="Mol")})
|
|
|
|
def failing_operation():
|
|
raise AppError(code="SOME_DOMAIN_ERROR", message="Bounded failure", status_code=422)
|
|
|
|
with pytest.raises(AppError):
|
|
JobService.run_sync_job(
|
|
db=db,
|
|
project_id=project_id,
|
|
job_type="test.bounded",
|
|
parameters={},
|
|
operation=failing_operation,
|
|
)
|
|
|
|
jobs = [item for item in db.added if isinstance(item, Job)]
|
|
assert jobs
|
|
assert jobs[-1].status == "failed"
|
|
assert jobs[-1].error_message == "Bounded failure"
|