|
19 | 19 |
|
20 | 20 | import pytest |
21 | 21 | from torchx.schedulers.api import AppDryRunInfo |
22 | | -from torchx.specs import AppDef, Role |
| 22 | +from torchx.specs import AppDef, AppState, Role |
23 | 23 |
|
24 | | -from nemo_run.core.execution.dgxcloud import DGXCloudExecutor |
25 | | -from nemo_run.run.torchx_backend.schedulers.dgxcloud import DGXCloudScheduler, create_scheduler |
| 24 | +from nemo_run.core.execution.dgxcloud import DGXCloudExecutor, DGXCloudState |
| 25 | +from nemo_run.run.torchx_backend.schedulers.dgxcloud import ( |
| 26 | + DGX_STATES, |
| 27 | + DGXCloudScheduler, |
| 28 | + create_scheduler, |
| 29 | +) |
26 | 30 |
|
27 | 31 |
|
28 | 32 | @pytest.fixture |
@@ -184,6 +188,56 @@ def test_log_iter(dgx_cloud_scheduler, dgx_cloud_executor): |
184 | 188 | assert logs == ["log2", "log3"] |
185 | 189 |
|
186 | 190 |
|
| 191 | +def test_unknown_state_maps_to_pending_not_failed(): |
| 192 | + # DGXCloudState.UNKNOWN must map to PENDING so transient API errors during |
| 193 | + # job startup do not cause wait_and_exit() to treat the job as terminal. |
| 194 | + assert DGX_STATES[DGXCloudState.UNKNOWN] == AppState.PENDING |
| 195 | + |
| 196 | + |
| 197 | +def test_describe_returns_pending_when_status_is_none(dgx_cloud_scheduler, dgx_cloud_executor): |
| 198 | + # Regression test: executor.status() returns None when the auth token is |
| 199 | + # missing or the API call fails transiently right after job submission. |
| 200 | + # describe() must return PENDING so the wait loop keeps polling. |
| 201 | + with ( |
| 202 | + mock.patch( |
| 203 | + "nemo_run.run.torchx_backend.schedulers.dgxcloud._get_job_dirs" |
| 204 | + ) as mock_get_job_dirs, |
| 205 | + mock.patch.object(DGXCloudExecutor, "status", return_value=None), |
| 206 | + ): |
| 207 | + mock_get_job_dirs.return_value = { |
| 208 | + "test_experiment___test_role___test_job_id": { |
| 209 | + "job_status": "RUNNING", |
| 210 | + "executor": dgx_cloud_executor, |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + response = dgx_cloud_scheduler.describe("test_experiment___test_role___test_job_id") |
| 215 | + assert response is not None |
| 216 | + assert response.state == AppState.PENDING |
| 217 | + |
| 218 | + |
| 219 | +def test_describe_returns_pending_when_status_is_unknown(dgx_cloud_scheduler, dgx_cloud_executor): |
| 220 | + # Regression test: the DGXCloud API transiently returns "Unknown" before a |
| 221 | + # job is visible (e.g. HTTP 404 right after submission). describe() must |
| 222 | + # return PENDING so the wait loop keeps polling instead of failing. |
| 223 | + with ( |
| 224 | + mock.patch( |
| 225 | + "nemo_run.run.torchx_backend.schedulers.dgxcloud._get_job_dirs" |
| 226 | + ) as mock_get_job_dirs, |
| 227 | + mock.patch.object(DGXCloudExecutor, "status", return_value=DGXCloudState.UNKNOWN), |
| 228 | + ): |
| 229 | + mock_get_job_dirs.return_value = { |
| 230 | + "test_experiment___test_role___test_job_id": { |
| 231 | + "job_status": "RUNNING", |
| 232 | + "executor": dgx_cloud_executor, |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + response = dgx_cloud_scheduler.describe("test_experiment___test_role___test_job_id") |
| 237 | + assert response is not None |
| 238 | + assert response.state == AppState.PENDING |
| 239 | + |
| 240 | + |
187 | 241 | def test_log_iter_str(dgx_cloud_scheduler, dgx_cloud_executor): |
188 | 242 | with mock.patch( |
189 | 243 | "nemo_run.run.torchx_backend.schedulers.dgxcloud._get_job_dirs" |
|
0 commit comments