diff --git a/tests/conftest.py b/tests/conftest.py index cd5052ea..e76e8c00 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -362,10 +362,20 @@ def tower_tasks_response(): @pytest.fixture def tower_workflow_response() -> TowerWorkflowResponse: - worfklow = TowerWorkflow(status="RUNNING") + workflow = TowerWorkflow(status="RUNNING") progress = TowerProgress(workflowProgress={}, processesProgress=[]) return TowerWorkflowResponse( - workflow=worfklow, + workflow=workflow, + progress=progress, + ) + + +@pytest.fixture +def tower_response_submitted() -> TowerWorkflowResponse: + workflow = TowerWorkflow(status="SUBMITTED") + progress = TowerProgress(workflowProgress={}, processesProgress=[]) + return TowerWorkflowResponse( + workflow=workflow, progress=progress, ) diff --git a/tests/services/conftest.py b/tests/services/conftest.py index b9dae5f5..046da301 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -1,9 +1,9 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock, Mock + import pytest from sqlalchemy.orm import Session - from trailblazer.clients.tower.tower_client import TowerAPIClient from trailblazer.constants import ( PRIORITY_OPTIONS, diff --git a/tests/services/test_job_service.py b/tests/services/test_job_service.py index 9a644168..6d9adba2 100644 --- a/tests/services/test_job_service.py +++ b/tests/services/test_job_service.py @@ -1,4 +1,8 @@ -from trailblazer.constants import TrailblazerStatus +import pytest + +from trailblazer.clients.tower.models import TowerWorkflowResponse +from trailblazer.constants import TrailblazerStatus, WorkflowManager +from trailblazer.exceptions import NoJobsError from trailblazer.services.job_service import JobService from trailblazer.store.models import Analysis @@ -57,3 +61,37 @@ def test_analysis_status_when_running_jobs( # THEN the status is running assert status == TrailblazerStatus.RUNNING + + +def test_fetch_pending_status_for_tower_without_jobs( + job_service: JobService, + tower_response_submitted: TowerWorkflowResponse, +): + """ + Verify that a Tower-managed analysis with no associated jobs returns a PENDING status + when Tower reports the workflow as submitted. + """ + # GIVEN a Tower analysis with workflow manager nf_tower and without jobs + analysis: Analysis = job_service.store.get_query(Analysis).first() + analysis.workflow_manager = WorkflowManager.TOWER + assert not analysis.jobs + + # GIVEN a simulated response from tower with the return status being submitted + job_service.tower_service.client.get_workflow.return_value = tower_response_submitted + + # WHEN fetching the analysis status + status: TrailblazerStatus = job_service.get_analysis_status(analysis.id) + + # THEN the analysis status should be PENDING + assert status == TrailblazerStatus.PENDING + + +def test_no_jobs_error_for_slurm_analysis(job_service: JobService, analysis_without_jobs: Analysis): + """ + Ensure that a NoJobsError is raised for a SLURM analysis with no associated jobs. + """ + # GIVEN an analysis without any associated jobs + # WHEN fetching the analysis status + # THEN a NoJobsError should be raised + with pytest.raises(NoJobsError): + job_service.get_analysis_status(analysis_without_jobs.id) diff --git a/trailblazer/constants.py b/trailblazer/constants.py index 6d9ed3af..086aeb0c 100644 --- a/trailblazer/constants.py +++ b/trailblazer/constants.py @@ -138,17 +138,32 @@ class TrailblazerStatusColor(StrEnum): RUNNING: str = "blue" -TOWER_WORKFLOW_STATUS: dict[str, str] = { - "ABORTED": TrailblazerStatus.FAILED, - "CACHED": TrailblazerStatus.COMPLETED, - "CANCELLED": TrailblazerStatus.CANCELLED, - "COMPLETED": TrailblazerStatus.COMPLETED, - "FAILED": TrailblazerStatus.FAILED, - "NEW": TrailblazerStatus.PENDING, - "RUNNING": TrailblazerStatus.RUNNING, - "SUBMITTED": TrailblazerStatus.PENDING, - "SUCCEEDED": TrailblazerStatus.COMPLETED, - "UNKNOWN": TrailblazerStatus.FAILED, +class TowerStatus(StrEnum): + """Tower statuses.""" + + ABORTED: str = "ABORTED" + CACHED: str = "CACHED" + CANCELLED: str = "CANCELLED" + COMPLETED: str = "COMPLETED" + FAILED: str = "FAILED" + NEW: str = "NEW" + RUNNING: str = "RUNNING" + SUBMITTED: str = "SUBMITTED" + SUCCEEDED: str = "SUCCEEDED" + UNKNOWN: str = "UNKNOWN" + + +TOWER_WORKFLOW_STATUS: dict[str, TrailblazerStatus] = { + TowerStatus.ABORTED: TrailblazerStatus.FAILED, + TowerStatus.CACHED: TrailblazerStatus.COMPLETED, + TowerStatus.CANCELLED: TrailblazerStatus.CANCELLED, + TowerStatus.COMPLETED: TrailblazerStatus.COMPLETED, + TowerStatus.FAILED: TrailblazerStatus.FAILED, + TowerStatus.NEW: TrailblazerStatus.PENDING, + TowerStatus.RUNNING: TrailblazerStatus.RUNNING, + TowerStatus.SUBMITTED: TrailblazerStatus.PENDING, + TowerStatus.SUCCEEDED: TrailblazerStatus.COMPLETED, + TowerStatus.UNKNOWN: TrailblazerStatus.FAILED, } diff --git a/trailblazer/dto/create_analysis_request.py b/trailblazer/dto/create_analysis_request.py index c6cc53c5..1abe7c5c 100644 --- a/trailblazer/dto/create_analysis_request.py +++ b/trailblazer/dto/create_analysis_request.py @@ -5,14 +5,14 @@ class CreateAnalysisRequest(BaseModel): case_id: str - email: str | None = None config_path: str - out_dir: str + email: str | None = None + is_hidden: bool | None = None order_id: int | None = None + out_dir: str priority: TrailblazerPriority - workflow: str | None = None ticket: str | None = None + tower_workflow_id: str | None = None type: TrailblazerTypes + workflow: str | None = None workflow_manager: WorkflowManager | None = None - tower_workflow_id: str | None = None - is_hidden: bool | None = None diff --git a/trailblazer/services/job_service/job_service.py b/trailblazer/services/job_service/job_service.py index ac95b76c..6c2f41e1 100644 --- a/trailblazer/services/job_service/job_service.py +++ b/trailblazer/services/job_service/job_service.py @@ -61,12 +61,12 @@ def get_analysis_status(self, analysis_id: int) -> TrailblazerStatus: if analysis.status == TrailblazerStatus.CANCELLED: return TrailblazerStatus.CANCELLED - if not analysis.jobs: - raise NoJobsError(f"No jobs found for analysis {analysis_id}") - if analysis.workflow_manager == WorkflowManager.TOWER: return self.tower_service.get_status(analysis_id) + if not analysis.jobs: + raise NoJobsError(f"No jobs found for analysis {analysis_id}") + return get_status(analysis.jobs) def get_analysis_progression(self, analysis_id: int) -> float: diff --git a/trailblazer/services/tower/tower_api_service.py b/trailblazer/services/tower/tower_api_service.py index 80258070..5210e2d2 100644 --- a/trailblazer/services/tower/tower_api_service.py +++ b/trailblazer/services/tower/tower_api_service.py @@ -31,7 +31,9 @@ def cancel_jobs(self, analysis_id: int) -> None: def get_status(self, analysis_id: int) -> TrailblazerStatus: analysis: Analysis = self.store.get_analysis_with_id(analysis_id) response = self.client.get_workflow(analysis.tower_workflow_id) - status = TOWER_WORKFLOW_STATUS.get(response.workflow.status, TrailblazerStatus.ERROR) + status: TrailblazerStatus = TOWER_WORKFLOW_STATUS.get( + response.workflow.status, TrailblazerStatus.ERROR + ) if status == TrailblazerStatus.COMPLETED: return TrailblazerStatus.QC return status