Skip to content

Commit 9542bc2

Browse files
committed
cover exceptions in tests
1 parent bc7ce89 commit 9542bc2

File tree

4 files changed

+132
-35
lines changed

4 files changed

+132
-35
lines changed

packages/pytest-simcore/src/pytest_simcore/helpers/async_jobs_server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
from dataclasses import dataclass
2+
13
from models_library.api_schemas_rpc_async_jobs.async_jobs import (
24
AsyncJobGet,
35
AsyncJobId,
46
AsyncJobNameData,
57
AsyncJobResult,
68
AsyncJobStatus,
79
)
10+
from models_library.api_schemas_rpc_async_jobs.exceptions import BaseAsyncjobRpcError
811
from models_library.progress_bar import ProgressReport
912
from models_library.rabbitmq_basic_types import RPCNamespace
1013
from pydantic import validate_call
1114
from pytest_mock import MockType
1215
from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient
1316

1417

18+
@dataclass
1519
class AsyncJobSideEffects:
20+
exception: BaseAsyncjobRpcError | None = None
1621

1722
@validate_call(config={"arbitrary_types_allowed": True})
1823
async def cancel(
@@ -23,7 +28,9 @@ async def cancel(
2328
job_id: AsyncJobId,
2429
job_id_data: AsyncJobNameData,
2530
) -> None:
26-
pass
31+
if self.exception is not None:
32+
raise self.exception
33+
return None
2734

2835
@validate_call(config={"arbitrary_types_allowed": True})
2936
async def status(
@@ -34,6 +41,9 @@ async def status(
3441
job_id: AsyncJobId,
3542
job_id_data: AsyncJobNameData,
3643
) -> AsyncJobStatus:
44+
if self.exception is not None:
45+
raise self.exception
46+
3747
return AsyncJobStatus(
3848
job_id=job_id,
3949
progress=ProgressReport(
@@ -53,6 +63,8 @@ async def result(
5363
job_id: AsyncJobId,
5464
job_id_data: AsyncJobNameData,
5565
) -> AsyncJobResult:
66+
if self.exception is not None:
67+
raise self.exception
5668
return AsyncJobResult(result="Success")
5769

5870
@validate_call(config={"arbitrary_types_allowed": True})
@@ -64,6 +76,8 @@ async def list_jobs(
6476
job_id_data: AsyncJobNameData,
6577
filter_: str = "",
6678
) -> list[AsyncJobGet]:
79+
if self.exception is not None:
80+
raise self.exception
6781
return [
6882
AsyncJobGet(
6983
job_id=AsyncJobId("123e4567-e89b-12d3-a456-426614174000"),

services/api-server/src/simcore_service_api_server/exceptions/task_errors.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,13 @@ class TaskMissingError(BaseBackEndError):
1313
status_code = status.HTTP_404_NOT_FOUND
1414

1515

16-
class TaskStatusError(BaseBackEndError):
17-
msg_template: str = "Could not get status of task {job_id}"
16+
class TaskResultMissingError(BaseBackEndError):
17+
msg_template: str = "Task {job_id} is not done"
1818
status_code = status.HTTP_404_NOT_FOUND
1919

2020

21-
class TaskNotDoneError(BaseBackEndError):
22-
msg_template: str = "Task {job_id} not done"
23-
status_code = status.HTTP_409_CONFLICT
24-
25-
2621
class TaskCancelledError(BaseBackEndError):
27-
msg_template: str = "Task {job_id} cancelled"
22+
msg_template: str = "Task {job_id} is cancelled"
2823
status_code = status.HTTP_409_CONFLICT
2924

3025

services/api-server/src/simcore_service_api_server/services_rpc/async_jobs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from simcore_service_api_server.exceptions.task_errors import (
2121
TaskCancelledError,
2222
TaskError,
23-
TaskNotDoneError,
23+
TaskResultMissingError,
2424
TaskSchedulerError,
2525
)
2626

@@ -68,7 +68,7 @@ async def status(
6868
@_exception_mapper(
6969
rpc_exception_map={
7070
JobSchedulerError: TaskSchedulerError,
71-
JobNotDoneError: TaskNotDoneError,
71+
JobNotDoneError: TaskResultMissingError,
7272
JobAbortedError: TaskCancelledError,
7373
JobError: TaskError,
7474
}

services/api-server/tests/unit/test_tasks.py

Lines changed: 112 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
from fastapi import status
66
from httpx import AsyncClient, BasicAuth
77
from models_library.api_schemas_long_running_tasks.tasks import TaskGet, TaskStatus
8+
from models_library.api_schemas_rpc_async_jobs.exceptions import (
9+
BaseAsyncjobRpcError,
10+
JobAbortedError,
11+
JobError,
12+
JobNotDoneError,
13+
JobSchedulerError,
14+
)
815
from pytest_mock import MockerFixture, MockType
916
from pytest_simcore.helpers.async_jobs_server import AsyncJobSideEffects
1017
from simcore_service_api_server.models.schemas.tasks import ApiServerEnvelope
@@ -13,8 +20,10 @@
1320

1421

1522
@pytest.fixture
16-
async def async_jobs_rpc_side_effects() -> Any:
17-
return AsyncJobSideEffects()
23+
async def async_jobs_rpc_side_effects(
24+
async_job_error: BaseAsyncjobRpcError | None,
25+
) -> Any:
26+
return AsyncJobSideEffects(exception=async_job_error)
1827

1928

2029
@pytest.fixture
@@ -51,48 +60,127 @@ def mocked_async_jobs_rpc_api(
5160
return mocks
5261

5362

63+
@pytest.mark.parametrize(
64+
"async_job_error, expected_status_code",
65+
[
66+
(None, status.HTTP_200_OK),
67+
(
68+
JobSchedulerError(
69+
exc=Exception("A very rare exception raised by the scheduler")
70+
),
71+
status.HTTP_500_INTERNAL_SERVER_ERROR,
72+
),
73+
],
74+
)
5475
async def test_get_async_jobs(
55-
client: AsyncClient, mocked_async_jobs_rpc_api: dict[str, MockType], auth: BasicAuth
76+
client: AsyncClient,
77+
mocked_async_jobs_rpc_api: dict[str, MockType],
78+
auth: BasicAuth,
79+
expected_status_code: int,
5680
):
5781

5882
response = await client.get("/v0/tasks", auth=auth)
59-
assert response.status_code == status.HTTP_200_OK
6083
assert mocked_async_jobs_rpc_api["list_jobs"].called
61-
result = ApiServerEnvelope[list[TaskGet]].model_validate_json(response.text)
62-
assert len(result.data) > 0
63-
assert all(isinstance(task, TaskGet) for task in result.data)
64-
task = result.data[0]
65-
assert task.abort_href == f"/v0/tasks/{task.task_id}:cancel"
66-
assert task.result_href == f"/v0/tasks/{task.task_id}/result"
67-
assert task.status_href == f"/v0/tasks/{task.task_id}"
68-
69-
84+
assert response.status_code == expected_status_code
85+
86+
if response.status_code == status.HTTP_200_OK:
87+
result = ApiServerEnvelope[list[TaskGet]].model_validate_json(response.text)
88+
assert len(result.data) > 0
89+
assert all(isinstance(task, TaskGet) for task in result.data)
90+
task = result.data[0]
91+
assert task.abort_href == f"/v0/tasks/{task.task_id}:cancel"
92+
assert task.result_href == f"/v0/tasks/{task.task_id}/result"
93+
assert task.status_href == f"/v0/tasks/{task.task_id}"
94+
95+
96+
@pytest.mark.parametrize(
97+
"async_job_error, expected_status_code",
98+
[
99+
(None, status.HTTP_200_OK),
100+
(
101+
JobSchedulerError(
102+
exc=Exception("A very rare exception raised by the scheduler")
103+
),
104+
status.HTTP_500_INTERNAL_SERVER_ERROR,
105+
),
106+
],
107+
)
70108
async def test_get_async_jobs_status(
71-
client: AsyncClient, mocked_async_jobs_rpc_api: dict[str, MockType], auth: BasicAuth
109+
client: AsyncClient,
110+
mocked_async_jobs_rpc_api: dict[str, MockType],
111+
auth: BasicAuth,
112+
expected_status_code: int,
72113
):
73114
task_id = f"{_faker.uuid4()}"
74115
response = await client.get(f"/v0/tasks/{task_id}", auth=auth)
75-
assert response.status_code == status.HTTP_200_OK
76116
assert mocked_async_jobs_rpc_api["status"].called
77117
assert f"{mocked_async_jobs_rpc_api['status'].call_args[1]['job_id']}" == task_id
78-
TaskStatus.model_validate_json(response.text)
79-
80-
118+
assert response.status_code == expected_status_code
119+
if response.status_code == status.HTTP_200_OK:
120+
TaskStatus.model_validate_json(response.text)
121+
122+
123+
@pytest.mark.parametrize(
124+
"async_job_error, expected_status_code",
125+
[
126+
(None, status.HTTP_204_NO_CONTENT),
127+
(
128+
JobSchedulerError(
129+
exc=Exception("A very rare exception raised by the scheduler")
130+
),
131+
status.HTTP_500_INTERNAL_SERVER_ERROR,
132+
),
133+
],
134+
)
81135
async def test_cancel_async_job(
82-
client: AsyncClient, mocked_async_jobs_rpc_api: dict[str, MockType], auth: BasicAuth
136+
client: AsyncClient,
137+
mocked_async_jobs_rpc_api: dict[str, MockType],
138+
auth: BasicAuth,
139+
expected_status_code: int,
83140
):
84141
task_id = f"{_faker.uuid4()}"
85142
response = await client.post(f"/v0/tasks/{task_id}:cancel", auth=auth)
86-
assert response.status_code == status.HTTP_204_NO_CONTENT
87143
assert mocked_async_jobs_rpc_api["cancel"].called
88144
assert f"{mocked_async_jobs_rpc_api['cancel'].call_args[1]['job_id']}" == task_id
89-
90-
145+
assert response.status_code == expected_status_code
146+
147+
148+
@pytest.mark.parametrize(
149+
"async_job_error, expected_status_code",
150+
[
151+
(None, status.HTTP_200_OK),
152+
(
153+
JobError(
154+
job_id=_faker.uuid4(),
155+
exc_type=Exception,
156+
exc_message="An exception from inside the async job",
157+
),
158+
status.HTTP_500_INTERNAL_SERVER_ERROR,
159+
),
160+
(
161+
JobNotDoneError(job_id=_faker.uuid4()),
162+
status.HTTP_404_NOT_FOUND,
163+
),
164+
(
165+
JobAbortedError(job_id=_faker.uuid4()),
166+
status.HTTP_409_CONFLICT,
167+
),
168+
(
169+
JobSchedulerError(
170+
exc=Exception("A very rare exception raised by the scheduler")
171+
),
172+
status.HTTP_500_INTERNAL_SERVER_ERROR,
173+
),
174+
],
175+
)
91176
async def test_get_async_job_result(
92-
client: AsyncClient, mocked_async_jobs_rpc_api: dict[str, MockType], auth: BasicAuth
177+
client: AsyncClient,
178+
mocked_async_jobs_rpc_api: dict[str, MockType],
179+
auth: BasicAuth,
180+
expected_status_code: int,
93181
):
94182
task_id = f"{_faker.uuid4()}"
95183
response = await client.get(f"/v0/tasks/{task_id}/result", auth=auth)
96-
assert response.status_code == status.HTTP_200_OK
184+
assert response.status_code == expected_status_code
97185
assert mocked_async_jobs_rpc_api["result"].called
98186
assert f"{mocked_async_jobs_rpc_api['result'].call_args[1]['job_id']}" == task_id

0 commit comments

Comments
 (0)