Skip to content

Commit 18fa279

Browse files
committed
Expand integration tests, fix transport response reading bug
Comprehensive integration tests covering all API endpoint groups: - Backends: list, get, field validation, degraded-optional - Characterizations: list, get by ID, field validation - Jobs: create, get, list, filter, pagination, cancel, delete, cost, probabilities, estimate, completed job fields, dry run - Sessions: list, create/end lifecycle, context manager - Whoami: sync, detailed - Usage: with permission skip - Async: whoami, list jobs, paginated iteration Fix bug in _transport.py where _parse_error_body accessed response.json()/response.text without calling response.read() first, causing ResponseNotRead on error status codes. 30 integration tests total (from 4). Tests that depend on account-specific features (sessions, usage) skip gracefully.
1 parent 250fb97 commit 18fa279

9 files changed

Lines changed: 357 additions & 32 deletions

File tree

ionq_core/_transport.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def _backoff_delays(max_retries: int) -> Iterator[float]:
4242

4343

4444
def _parse_error_body(response: httpx.Response) -> dict | str | None:
45+
response.read()
4546
try:
4647
return response.json()
4748
except Exception:

tests/integration/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
import contextlib
44
import os
5+
import warnings
56

67
import pytest
78

89
from ionq_core import AuthenticatedClient, IonQClient
910
from ionq_core.api.default import delete_job
1011

12+
# Integration tests make real HTTPS connections that may leak sockets during
13+
# process teardown. This is harmless but triggers pytest's filterwarnings=error.
14+
warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*SSL")
15+
warnings.filterwarnings("ignore", category=pytest.PytestUnraisableExceptionWarning, message=".*SSL.*")
16+
1117
_job_ids: list[str] = []
1218

1319

tests/integration/test_async.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Integration tests verifying async variants work against the real API."""
2+
3+
import pytest
4+
5+
from ionq_core import IonQClient, aiter_jobs
6+
from ionq_core.api.default import get_jobs
7+
from ionq_core.api.whoami import get_whoami
8+
9+
pytestmark = pytest.mark.integration
10+
11+
12+
@pytest.fixture
13+
def async_client(api_key):
14+
return IonQClient(api_key=api_key)
15+
16+
17+
async def test_async_whoami(async_client):
18+
async with async_client as c:
19+
result = await get_whoami.asyncio(client=c)
20+
assert result is not None
21+
assert result.key_name
22+
23+
24+
async def test_async_list_jobs(async_client):
25+
async with async_client as c:
26+
resp = await get_jobs.asyncio(client=c, limit=2)
27+
assert resp is not None
28+
assert len(resp.jobs) > 0
29+
30+
31+
async def test_async_iter_jobs(async_client):
32+
async with async_client as c:
33+
jobs = []
34+
async for j in aiter_jobs(c, limit=1):
35+
jobs.append(j)
36+
if len(jobs) >= 2:
37+
break
38+
assert len(jobs) == 2

tests/integration/test_backends.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
1-
"""Verify backend listing works against the real API."""
1+
"""Integration tests for backend endpoints."""
2+
3+
import warnings
24

35
import pytest
46

57
from ionq_core import Client
6-
from ionq_core.api.backends import get_backends
8+
from ionq_core.api.backends import get_backend, get_backends
79

810
pytestmark = pytest.mark.integration
911

12+
BASE_URL = "https://api.ionq.co/v0.4"
13+
14+
15+
def _unauthenticated():
16+
"""Unauthenticated client (no managed transport, may leak sockets)."""
17+
return Client(base_url=BASE_URL)
18+
19+
20+
class TestListBackends:
21+
def test_returns_backends(self):
22+
with warnings.catch_warnings():
23+
warnings.simplefilter("ignore", ResourceWarning)
24+
backends = get_backends.sync(client=_unauthenticated())
25+
assert backends is not None
26+
assert len(backends) > 0
27+
28+
def test_has_qpu(self):
29+
with warnings.catch_warnings():
30+
warnings.simplefilter("ignore", ResourceWarning)
31+
backends = get_backends.sync(client=_unauthenticated())
32+
names = [b.backend for b in backends]
33+
assert any("qpu" in n for n in names)
34+
35+
def test_backend_fields(self):
36+
with warnings.catch_warnings():
37+
warnings.simplefilter("ignore", ResourceWarning)
38+
backends = get_backends.sync(client=_unauthenticated())
39+
for b in backends:
40+
assert b.backend
41+
assert b.status
42+
assert b.qubits > 0
43+
assert b.average_queue_time is not None
44+
assert b.last_updated
45+
1046

11-
def test_list_backends():
12-
"""Backends endpoint is unauthenticated - use a plain Client."""
13-
unauthenticated = Client(base_url="https://api.ionq.co/v0.4")
14-
backends = get_backends.sync(client=unauthenticated)
15-
assert backends is not None
16-
assert len(backends) > 0
17-
names = [b.backend for b in backends]
18-
assert any("qpu" in n for n in names), f"Expected at least one QPU backend in {names}"
47+
class TestGetBackend:
48+
def test_get_qpu(self, client):
49+
backend = get_backend.sync("qpu.forte-1", client=client)
50+
assert backend is not None
51+
assert backend.backend == "qpu.forte-1"
52+
assert backend.qubits > 0
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Integration tests for characterization endpoints."""
2+
3+
import pytest
4+
5+
from ionq_core.api.characterizations import get_characterization, get_characterizations_for_backend
6+
7+
pytestmark = pytest.mark.integration
8+
9+
10+
class TestListCharacterizations:
11+
def test_returns_characterizations(self, client):
12+
resp = get_characterizations_for_backend.sync("qpu.forte-1", client=client, limit=1)
13+
assert resp is not None
14+
assert len(resp.characterizations) > 0
15+
16+
def test_characterization_fields(self, client):
17+
resp = get_characterizations_for_backend.sync("qpu.forte-1", client=client, limit=1)
18+
char = resp.characterizations[0]
19+
assert char.id
20+
assert char.backend == "qpu.forte-1"
21+
assert char.date
22+
23+
24+
class TestGetCharacterization:
25+
def test_get_by_id(self, client):
26+
resp = get_characterizations_for_backend.sync("qpu.forte-1", client=client, limit=1)
27+
char_id = resp.characterizations[0].id
28+
29+
char = get_characterization.sync("qpu.forte-1", char_id, client=client)
30+
assert char is not None
31+
assert char.id == char_id
32+
assert char.qubits > 0

tests/integration/test_sessions.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Integration tests for session endpoints."""
2+
3+
import contextlib
4+
5+
import pytest
6+
7+
from ionq_core import SessionManager
8+
from ionq_core._exceptions import NotFoundError
9+
from ionq_core.api.default import get_session_jobs, get_sessions
10+
11+
pytestmark = pytest.mark.integration
12+
13+
14+
def test_list_sessions(client):
15+
try:
16+
result = get_sessions.sync(client=client)
17+
assert result is not None
18+
except NotFoundError:
19+
pytest.skip("Sessions endpoint not available for this account")
20+
21+
22+
class TestSessionManager:
23+
def test_create_and_end_session(self, client):
24+
mgr = SessionManager(client, "simulator")
25+
try:
26+
mgr.open()
27+
except NotFoundError:
28+
pytest.skip("Sessions not available for this account")
29+
30+
try:
31+
assert mgr.session_id is not None
32+
33+
session = mgr.status()
34+
assert session is not None
35+
assert session.backend == "simulator"
36+
37+
jobs = get_session_jobs.sync(mgr.session_id, client=client)
38+
assert jobs is not None
39+
finally:
40+
with contextlib.suppress(Exception):
41+
mgr.close()
42+
43+
def test_context_manager(self, client):
44+
mgr = SessionManager(client, "simulator")
45+
try:
46+
mgr.open()
47+
except NotFoundError:
48+
pytest.skip("Sessions not available for this account")
49+
try:
50+
assert mgr.session_id is not None
51+
status = mgr.status()
52+
assert status is not None
53+
finally:
54+
with contextlib.suppress(Exception):
55+
mgr.close()

0 commit comments

Comments
 (0)