Skip to content

Commit fa7c7ce

Browse files
committed
reach 100% coverage
1 parent 2ad5cad commit fa7c7ce

File tree

4 files changed

+166
-2
lines changed

4 files changed

+166
-2
lines changed

antares-python/src/antares/client/tcp.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import asyncio
22
import json
3+
import logging
34
from collections.abc import AsyncIterator
45

56
from antares.errors import SubscriptionError
67

8+
logger = logging.getLogger(__name__)
9+
710

811
class TCPSubscriber:
912
"""
@@ -43,10 +46,13 @@ async def subscribe(self) -> AsyncIterator[dict]:
4346
asyncio.IncompleteReadError,
4447
json.JSONDecodeError,
4548
) as e:
46-
raise SubscriptionError(f"Failed to read from TCP stream: {e}") from e
49+
logger.error("TCP stream error: %s", e)
50+
if not self.reconnect:
51+
raise SubscriptionError(f"Failed to read from TCP stream: {e}") from e
4752

53+
# Stop if not reconnecting
4854
if not self.reconnect:
4955
break
5056

51-
# Wait before attempting to reconnect
57+
logger.info("Waiting 1 second before retrying TCP connection...")
5258
await asyncio.sleep(1)

antares-python/tests/client/test_rest.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,41 @@ def test_add_ship_invalid_response(mocker):
4444
client = RestClient(base_url="http://localhost")
4545
with pytest.raises(SimulationError):
4646
client.add_ship(ship)
47+
48+
49+
def test_reset_simulation_http_error(mocker):
50+
request = httpx.Request("POST", "http://localhost/simulation/reset")
51+
response = httpx.Response(500, content=b"internal error", request=request)
52+
53+
mock_post = mocker.patch("httpx.post", return_value=response)
54+
55+
# .raise_for_status() triggers HTTPStatusError
56+
def raise_error():
57+
raise httpx.HTTPStatusError("error", request=request, response=response)
58+
59+
response.raise_for_status = raise_error
60+
61+
client = RestClient(base_url="http://localhost")
62+
63+
with pytest.raises(SimulationError) as exc:
64+
client.reset_simulation()
65+
66+
assert "Reset failed" in str(exc.value)
67+
mock_post.assert_called_once()
68+
69+
70+
def test_add_ship_request_error(mocker):
71+
mocker.patch(
72+
"httpx.post",
73+
side_effect=httpx.RequestError(
74+
"connection dropped", request=httpx.Request("POST", "http://localhost/simulation/ships")
75+
),
76+
)
77+
78+
ship = ShipConfig(initial_position=(0, 0))
79+
client = RestClient(base_url="http://localhost")
80+
81+
with pytest.raises(ConnectionError) as exc:
82+
client.add_ship(ship)
83+
84+
assert "Could not reach Antares API" in str(exc.value)

antares-python/tests/client/test_tcp.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,39 @@ async def test_subscribe_failure(monkeypatch):
3838
with pytest.raises(SubscriptionError):
3939
async for _ in subscriber.subscribe():
4040
pass
41+
42+
43+
@pytest.mark.asyncio
44+
async def test_subscribe_reconnects_on_failure(monkeypatch):
45+
class OneMessageReader:
46+
def __init__(self):
47+
self.called = False
48+
49+
def at_eof(self):
50+
return self.called
51+
52+
async def readline(self):
53+
if not self.called:
54+
self.called = True
55+
return b'{"event": "recovered"}\n'
56+
return b""
57+
58+
open_calls = []
59+
60+
async def fake_open_connection(host, port):
61+
if not open_calls:
62+
open_calls.append("fail")
63+
raise ConnectionRefusedError("initial fail")
64+
return OneMessageReader(), None
65+
66+
monkeypatch.setattr("asyncio.open_connection", fake_open_connection)
67+
monkeypatch.setattr("asyncio.sleep", AsyncMock())
68+
69+
subscriber = TCPSubscriber("localhost", 1234, reconnect=True)
70+
71+
events = []
72+
async for event in subscriber.subscribe():
73+
events.append(event)
74+
break # Exit after one event
75+
76+
assert events == [{"event": "recovered"}]

antares-python/tests/test_cli.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typer.testing import CliRunner
55

66
from antares.cli import app
7+
from antares.errors import ConnectionError, SimulationError, SubscriptionError
78

89
runner = CliRunner()
910

@@ -50,3 +51,86 @@ async def fake_sub(self):
5051
result = runner.invoke(app, ["subscribe", "--config", fake_config])
5152
assert result.exit_code == 0
5253
assert "test-event" in result.output
54+
55+
56+
def test_handle_error_json(monkeypatch):
57+
result = runner.invoke(app, ["reset", "--json"], catch_exceptions=False)
58+
assert result.exit_code in {1, 2}
59+
assert "error" in result.output
60+
61+
62+
def test_build_client_fails(mocker):
63+
mocker.patch("antares.config_loader.load_config", side_effect=Exception("broken config"))
64+
result = runner.invoke(app, ["reset", "--config", "invalid.toml"])
65+
assert result.exit_code == 1
66+
assert "Failed to load configuration" in result.output
67+
68+
69+
def test_cli_reset_error_handling(mocker, fake_config):
70+
mocker.patch(
71+
"antares.client.rest.RestClient.reset_simulation",
72+
side_effect=ConnectionError("cannot connect"),
73+
)
74+
result = runner.invoke(app, ["reset", "--config", fake_config])
75+
expected_exit_code = 2
76+
assert result.exit_code == expected_exit_code
77+
assert "cannot connect" in result.output
78+
79+
80+
def test_cli_add_ship_error_handling(mocker, fake_config):
81+
mocker.patch(
82+
"antares.client.rest.RestClient.add_ship", side_effect=SimulationError("ship rejected")
83+
)
84+
result = runner.invoke(app, ["add-ship", "--x", "1", "--y", "2", "--config", fake_config])
85+
expected_exit_code = 2
86+
assert result.exit_code == expected_exit_code
87+
assert "ship rejected" in result.output
88+
89+
90+
def test_cli_subscribe_error(monkeypatch, fake_config):
91+
class FailingAsyncGenerator:
92+
def __aiter__(self):
93+
return self
94+
95+
async def __anext__(self):
96+
raise SubscriptionError("stream failed")
97+
98+
monkeypatch.setattr(
99+
"antares.client.tcp.TCPSubscriber.subscribe", lambda self: FailingAsyncGenerator()
100+
)
101+
102+
result = runner.invoke(app, ["subscribe", "--config", fake_config])
103+
expected_exit_code = 3
104+
assert result.exit_code == expected_exit_code
105+
assert "stream failed" in result.output
106+
107+
108+
def test_cli_verbose_prints_config(mocker, fake_config):
109+
mocker.patch("antares.client.tcp.TCPSubscriber.subscribe", return_value=iter([]))
110+
mocker.patch("antares.client.rest.RestClient.reset_simulation")
111+
112+
result = runner.invoke(app, ["reset", "--config", fake_config, "--verbose"])
113+
assert result.exit_code == 0
114+
assert "Using settings" in result.output
115+
116+
117+
def test_cli_subscribe_json(monkeypatch, fake_config):
118+
class OneEventGen:
119+
def __init__(self):
120+
self.done = False
121+
122+
def __aiter__(self):
123+
return self
124+
125+
async def __anext__(self):
126+
if not self.done:
127+
self.done = True
128+
return {"event": "test"}
129+
raise StopAsyncIteration
130+
131+
monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", lambda self: OneEventGen())
132+
133+
result = runner.invoke(app, ["subscribe", "--config", fake_config, "--json"])
134+
135+
assert result.exit_code == 0
136+
assert '{"event": "test"}' in result.output

0 commit comments

Comments
 (0)