| """AMD Dev Cloud REST client — stubs the httpx transport, no network. |
| |
| `httpx.MockTransport` is built into httpx (no `respx` dep needed). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from typing import Any |
|
|
| import httpx |
| import pytest |
|
|
| from mindxtrain.deploy.amd_dev_cloud import ( |
| DEFAULT_BASE, |
| AmdDevCloudAuthError, |
| AmdDevCloudClient, |
| AmdDevCloudConfig, |
| AmdDevCloudError, |
| build_create_payload, |
| extract_public_ip, |
| from_env, |
| missing_env, |
| ) |
|
|
|
|
| def _cfg(**ov: Any) -> AmdDevCloudConfig: |
| base = {"token": "dop_v1_TEST", "ssh_key_id": 56216059} |
| base.update(ov) |
| return AmdDevCloudConfig(**base) |
|
|
|
|
| def _client_with_handler(handler, *, cfg: AmdDevCloudConfig | None = None) -> AmdDevCloudClient: |
| cfg = cfg or _cfg() |
| transport = httpx.MockTransport(handler) |
| return AmdDevCloudClient( |
| cfg, |
| client=httpx.Client( |
| base_url=cfg.api_base, |
| headers={"Authorization": f"Bearer {cfg.token}"}, |
| transport=transport, |
| ), |
| ) |
|
|
|
|
| def test_missing_env_lists_required() -> None: |
| miss = missing_env({}) |
| assert "AMD_DEV_CLOUD_TOKEN" in miss |
| assert "AMD_DEV_CLOUD_SSH_KEY_ID" in miss |
| full = missing_env({"AMD_DEV_CLOUD_TOKEN": "x", "AMD_DEV_CLOUD_SSH_KEY_ID": "1"}) |
| assert full == [] |
|
|
|
|
| def test_from_env_parses_int_ssh_key_id() -> None: |
| cfg = from_env({ |
| "AMD_DEV_CLOUD_TOKEN": "x", |
| "AMD_DEV_CLOUD_SSH_KEY_ID": "56216059", |
| "AMD_DEV_CLOUD_TAGS": "a, b ,c", |
| }) |
| assert cfg.ssh_key_id == 56216059 |
| assert cfg.tags == ("a", "b", "c") |
| assert cfg.api_base == DEFAULT_BASE |
|
|
|
|
| def test_from_env_rejects_non_integer_ssh_key_id() -> None: |
| with pytest.raises(RuntimeError, match="must be an int"): |
| from_env({"AMD_DEV_CLOUD_TOKEN": "x", "AMD_DEV_CLOUD_SSH_KEY_ID": "notanint"}) |
|
|
|
|
| def test_build_create_payload_matches_curl_example() -> None: |
| """Pin the exact JSON body the user supplied in the spec.""" |
| cfg = _cfg( |
| region="atl1", |
| size="gpu-mi300x8-1536gb-devcloud", |
| image="vllm-0-17-1", |
| ssh_key_id=56216059, |
| tags=("mindx", "train", "aglm", "agenticplace", "pythai"), |
| ) |
| body = build_create_payload(cfg, name="mindxtrain", user_data="") |
| |
| assert body == { |
| "name": "mindxtrain", |
| "region": "atl1", |
| "size": "gpu-mi300x8-1536gb-devcloud", |
| "image": "vllm-0-17-1", |
| "ssh_keys": [56216059], |
| "backups": False, |
| "ipv6": True, |
| "monitoring": True, |
| "tags": ["mindx", "train", "aglm", "agenticplace", "pythai"], |
| "user_data": "", |
| "vpc_uuid": "", |
| } |
|
|
|
|
| def test_create_returns_droplet_dict() -> None: |
| captured = {} |
|
|
| def handler(request: httpx.Request) -> httpx.Response: |
| assert request.method == "POST" |
| assert request.url.path == "/v2/droplets" |
| captured["auth"] = request.headers.get("authorization") |
| captured["body"] = json.loads(request.content) |
| return httpx.Response(202, json={"droplet": {"id": 12345, "status": "new", "name": "mindxtrain"}}) |
|
|
| with _client_with_handler(handler) as c: |
| droplet = c.create(name="mindxtrain", user_data="#cloud-config\n") |
| assert droplet["id"] == 12345 |
| assert captured["auth"] == "Bearer dop_v1_TEST" |
| assert captured["body"]["name"] == "mindxtrain" |
| assert captured["body"]["user_data"] == "#cloud-config\n" |
|
|
|
|
| def test_create_401_raises_auth_error() -> None: |
| def handler(_request: httpx.Request) -> httpx.Response: |
| return httpx.Response(401, json={"message": "bad token"}) |
|
|
| with _client_with_handler(handler) as c: |
| with pytest.raises(AmdDevCloudAuthError, match="auth failed"): |
| c.create() |
|
|
|
|
| def test_create_500_raises_generic_error() -> None: |
| def handler(_request: httpx.Request) -> httpx.Response: |
| return httpx.Response(500, json={"message": "boom"}) |
|
|
| with _client_with_handler(handler) as c: |
| with pytest.raises(AmdDevCloudError, match="500"): |
| c.create() |
|
|
|
|
| def test_poll_until_active_returns_when_status_active() -> None: |
| statuses = iter(["new", "new", "active"]) |
|
|
| def handler(_request: httpx.Request) -> httpx.Response: |
| s = next(statuses) |
| body = {"droplet": { |
| "id": 1, "status": s, |
| "networks": {"v4": [{"type": "public", "ip_address": "1.2.3.4"}]}, |
| }} |
| return httpx.Response(200, json=body) |
|
|
| sleeps: list[float] = [] |
| with _client_with_handler(handler) as c: |
| droplet = c.poll_until_active(1, sleep=sleeps.append, interval=5.0, timeout=600) |
| assert droplet["status"] == "active" |
| |
| assert sleeps == [5.0, 5.0] |
|
|
|
|
| def test_poll_until_active_raises_on_timeout() -> None: |
| def handler(_request: httpx.Request) -> httpx.Response: |
| return httpx.Response(200, json={"droplet": {"id": 1, "status": "new"}}) |
|
|
| |
| times = iter([0.0, 0.0, 1000.0, 1000.0, 1000.0]) |
|
|
| with _client_with_handler(handler) as c: |
| with pytest.raises(TimeoutError, match="did not reach 'active'"): |
| c.poll_until_active(1, timeout=10.0, interval=1.0, sleep=lambda _s: None, now=lambda: next(times)) |
|
|
|
|
| def test_poll_until_active_raises_on_terminal_state() -> None: |
| def handler(_request: httpx.Request) -> httpx.Response: |
| return httpx.Response(200, json={"droplet": {"id": 1, "status": "errored"}}) |
|
|
| with _client_with_handler(handler) as c: |
| with pytest.raises(AmdDevCloudError, match="terminal state"): |
| c.poll_until_active(1, sleep=lambda _s: None, now=lambda: 0.0, timeout=60) |
|
|
|
|
| def test_extract_public_ip_handles_no_v4() -> None: |
| assert extract_public_ip({"networks": {"v4": []}}) is None |
| assert extract_public_ip({"networks": {}}) is None |
| assert extract_public_ip({}) is None |
|
|
|
|
| def test_extract_public_ip_prefers_public_type() -> None: |
| droplet = {"networks": {"v4": [ |
| {"type": "private", "ip_address": "10.0.0.1"}, |
| {"type": "public", "ip_address": "1.2.3.4"}, |
| ]}} |
| assert extract_public_ip(droplet) == "1.2.3.4" |
|
|
|
|
| def test_list_filters_by_name() -> None: |
| def handler(_request: httpx.Request) -> httpx.Response: |
| return httpx.Response(200, json={"droplets": [ |
| {"id": 1, "name": "mindxtrain"}, |
| {"id": 2, "name": "other"}, |
| ]}) |
|
|
| with _client_with_handler(handler) as c: |
| all_ = c.list() |
| only = c.list(name="mindxtrain") |
| assert len(all_) == 2 |
| assert len(only) == 1 |
| assert only[0]["id"] == 1 |
|
|