Skip to content

Commit 2ad5cad

Browse files
committed
improve tests coverage
1 parent 565aab3 commit 2ad5cad

File tree

11 files changed

+85
-66
lines changed

11 files changed

+85
-66
lines changed

antares-python/antares.log

Whitespace-only changes.

antares-python/pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ readme = "README.md"
99
requires-python = ">=3.13"
1010
dependencies = [
1111
"pydantic>=2.11.3",
12+
"pydantic-settings>=2.8.1",
1213
"httpx>=0.28.1",
1314
"typer>=0.15.2",
1415
"rich>=13.0",
1516
"tomli>=2.2.1",
1617
]
1718

19+
[project.optional-dependencies]
20+
dev = ["pytest-asyncio>=0.26.0"]
21+
1822
[project.scripts]
1923
antares = "antares.cli:app"
2024

@@ -34,6 +38,8 @@ files = ["src"]
3438

3539
[tool.pytest.ini_options]
3640
pythonpath = "src"
41+
asyncio_mode = "auto"
42+
asyncio_default_fixture_loop_scope = "function"
3743
addopts = "-ra -q -ra -q --cov=src --cov-report=term-missing"
3844

3945
[tool.setuptools.packages.find]
@@ -47,5 +53,5 @@ test = "pytest"
4753
coverage = "pytest --cov=src --cov-report=term-missing"
4854
build = "python -m build"
4955
publish = "twine upload dist/* --repository antares-python"
50-
check = "task lint && task typecheck && task test"
56+
check = "task lint && task format && task typecheck && task test"
5157
release = "task check && task build && task publish"

antares-python/src/antares/cli.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
import sys
21
import asyncio
32
import json
4-
import typer
53
import logging
4+
5+
import typer
66
from rich.console import Console
77
from rich.theme import Theme
88

9-
from antares import ShipConfig, AntaresClient
9+
from antares import AntaresClient, ShipConfig
1010
from antares.config_loader import load_config
11-
from antares.errors import (
12-
ConnectionError,
13-
SimulationError,
14-
SubscriptionError
15-
)
11+
from antares.errors import ConnectionError, SimulationError, SubscriptionError
1612
from antares.logger import setup_logging
1713

18-
app = typer.Typer(name="antares", help="Antares CLI for ship simulation")
19-
console = Console(theme=Theme({
20-
"info": "green",
21-
"warn": "yellow",
22-
"error": "bold red"
23-
}))
14+
app = typer.Typer(name="antares", help="Antares CLI for ship simulation", no_args_is_help=True)
15+
console = Console(theme=Theme({"info": "green", "warn": "yellow", "error": "bold red"}))
2416

2517

2618
def handle_error(message: str, code: int, json_output: bool = False):
@@ -57,7 +49,7 @@ def build_client(config_path: str | None, verbose: bool, json_output: bool) -> A
5749
def reset(
5850
config: str = typer.Option(None),
5951
verbose: bool = typer.Option(False, "--verbose", "-v"),
60-
json_output: bool = typer.Option(False, "--json", help="Output in JSON format")
52+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
6153
):
6254
client = build_client(config, verbose, json_output)
6355
try:
@@ -70,11 +62,11 @@ def reset(
7062

7163
@app.command()
7264
def add_ship(
73-
x: float,
74-
y: float,
75-
config: str = typer.Option(None),
76-
verbose: bool = typer.Option(False, "--verbose", "-v"),
77-
json_output: bool = typer.Option(False, "--json", help="Output in JSON format")
65+
x: float = typer.Option(..., help="X coordinate of the ship"),
66+
y: float = typer.Option(..., help="Y coordinate of the ship"),
67+
config: str = typer.Option(None, help="Path to the configuration file"),
68+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
69+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
7870
):
7971
client = build_client(config, verbose, json_output)
8072
try:
@@ -91,7 +83,7 @@ def subscribe(
9183
config: str = typer.Option(None),
9284
verbose: bool = typer.Option(False, "--verbose", "-v"),
9385
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
94-
log_file: str = typer.Option("antares.log", help="Path to log file")
86+
log_file: str = typer.Option("antares.log", help="Path to log file"),
9587
):
9688
setup_logging(log_file=log_file, level=logging.DEBUG if verbose else logging.INFO)
9789
logger = logging.getLogger("antares.cli")

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,24 @@ def __init__(
2020
Accepts config overrides directly or falls back to environment-based configuration.
2121
"""
2222

23+
overrides = {
24+
"base_url": base_url,
25+
"tcp_host": tcp_host,
26+
"tcp_port": tcp_port,
27+
"timeout": timeout,
28+
"auth_token": auth_token,
29+
}
30+
clean_overrides = {k: v for k, v in overrides.items() if v is not None}
31+
2332
# Merge provided arguments with environment/.env via AntaresSettings
24-
self._settings = AntaresSettings(
25-
base_url=base_url,
26-
tcp_host=tcp_host,
27-
tcp_port=tcp_port,
28-
timeout=timeout,
29-
auth_token=auth_token,
30-
)
33+
self._settings = AntaresSettings(**clean_overrides)
3134

3235
self._rest = RestClient(
3336
base_url=self._settings.base_url,
3437
timeout=self._settings.timeout,
3538
auth_token=self._settings.auth_token,
3639
)
37-
self._tcp = TCPSubscriber(
38-
host=self._settings.tcp_host, port=self._settings.tcp_port
39-
)
40+
self._tcp = TCPSubscriber(host=self._settings.tcp_host, port=self._settings.tcp_port)
4041

4142
def reset_simulation(self) -> None:
4243
"""

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ class RestClient:
99
Internal client for interacting with the Antares simulation REST API.
1010
"""
1111

12-
def __init__(
13-
self, base_url: str, timeout: float = 5.0, auth_token: str | None = None
14-
) -> None:
12+
def __init__(self, base_url: str, timeout: float = 5.0, auth_token: str | None = None) -> None:
1513
"""
1614
Initializes the REST client.
1715
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pydantic import BaseSettings, Field
1+
from pydantic_settings import BaseSettings, SettingsConfigDict
22

33

44
class AntaresSettings(BaseSettings):
@@ -7,12 +7,14 @@ class AntaresSettings(BaseSettings):
77
Supports environment variables and `.env` file loading.
88
"""
99

10-
base_url: str = Field(..., env="ANTARES_BASE_URL")
11-
tcp_host: str = Field("localhost", env="ANTARES_TCP_HOST")
12-
tcp_port: int = Field(9000, env="ANTARES_TCP_PORT")
13-
timeout: float = Field(5.0, env="ANTARES_TIMEOUT")
14-
auth_token: str | None = Field(None, env="ANTARES_AUTH_TOKEN")
10+
base_url: str
11+
tcp_host: str = "localhost"
12+
tcp_port: int = 9000
13+
timeout: float = 5.0
14+
auth_token: str | None = None
1515

16-
class Config:
17-
env_file = ".env"
18-
case_sensitive = False
16+
model_config = SettingsConfigDict(
17+
env_file=".env",
18+
env_prefix="ANTARES_",
19+
case_sensitive=False,
20+
)

antares-python/src/antares/logger.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22
from pathlib import Path
3+
34
from rich.logging import RichHandler
45

6+
57
def setup_logging(log_file: str = "antares.log", level: int = logging.INFO) -> None:
68
"""Configure logging to both rich console and a file."""
79
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
@@ -12,6 +14,6 @@ def setup_logging(log_file: str = "antares.log", level: int = logging.INFO) -> N
1214
datefmt="[%Y-%m-%d %H:%M:%S]",
1315
handlers=[
1416
RichHandler(rich_tracebacks=True, show_path=False),
15-
logging.FileHandler(log_file, encoding="utf-8")
16-
]
17+
logging.FileHandler(log_file, encoding="utf-8"),
18+
],
1719
)

antares-python/tests/client/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_add_ship_delegates(mocker):
2121

2222
@pytest.mark.asyncio
2323
async def test_subscribe_delegates(monkeypatch):
24-
async def fake_subscribe():
24+
async def fake_subscribe(_self):
2525
yield {"event": "test"}
2626

2727
monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", fake_subscribe)

antares-python/tests/client/test_rest.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88

99
def test_reset_simulation_success(mocker):
10-
mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200))
10+
mock_request = httpx.Request("POST", "http://localhost/simulation/reset")
11+
mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200, request=mock_request))
1112
client = RestClient(base_url="http://localhost")
1213
client.reset_simulation()
1314
mock_post.assert_called_once()
@@ -21,15 +22,24 @@ def test_reset_simulation_failure(mocker):
2122

2223

2324
def test_add_ship_success(mocker):
24-
mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200))
25+
mock_request = httpx.Request("POST", "http://localhost/simulation/ships")
26+
mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200, request=mock_request))
2527
ship = ShipConfig(initial_position=(0, 0))
2628
client = RestClient(base_url="http://localhost")
2729
client.add_ship(ship)
2830
mock_post.assert_called_once()
2931

3032

3133
def test_add_ship_invalid_response(mocker):
32-
mocker.patch("httpx.post", return_value=httpx.Response(400, content=b"bad request"))
34+
mock_request = httpx.Request("POST", "http://localhost/simulation/ships")
35+
mocker.patch(
36+
"httpx.post",
37+
return_value=httpx.Response(
38+
400,
39+
content=b"bad request",
40+
request=mock_request,
41+
),
42+
)
3343
ship = ShipConfig(initial_position=(0, 0))
3444
client = RestClient(base_url="http://localhost")
3545
with pytest.raises(SimulationError):

antares-python/tests/client/test_tcp.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,31 @@
88

99
@pytest.mark.asyncio
1010
async def test_subscribe_success(monkeypatch):
11+
# Simulated lines returned from the TCP stream
12+
lines = [b'{"event": "ok"}\n', b'{"event": "done"}\n', b""]
13+
14+
async def fake_readline():
15+
return lines.pop(0)
16+
17+
# Simulate EOF after all lines are read
18+
eof_flags = [False, False, True]
19+
1120
fake_reader = AsyncMock()
12-
fake_reader.readline = AsyncMock(
13-
side_effect=[b'{"event": "ok"}\n', b'{"event": "done"}\n', b""]
14-
)
15-
fake_reader.at_eof = MagicMock(return_value=False)
21+
fake_reader.readline = AsyncMock(side_effect=fake_readline)
22+
fake_reader.at_eof = MagicMock(side_effect=eof_flags)
1623

17-
monkeypatch.setattr(
18-
"asyncio.open_connection", AsyncMock(return_value=(fake_reader, None))
19-
)
24+
# Patch asyncio.open_connection to return our mocked reader
25+
monkeypatch.setattr("asyncio.open_connection", AsyncMock(return_value=(fake_reader, None)))
2026

2127
subscriber = TCPSubscriber("localhost", 1234, reconnect=False)
28+
2229
events = [event async for event in subscriber.subscribe()]
2330
assert events == [{"event": "ok"}, {"event": "done"}]
2431

2532

2633
@pytest.mark.asyncio
2734
async def test_subscribe_failure(monkeypatch):
28-
monkeypatch.setattr(
29-
"asyncio.open_connection", AsyncMock(side_effect=ConnectionRefusedError())
30-
)
35+
monkeypatch.setattr("asyncio.open_connection", AsyncMock(side_effect=ConnectionRefusedError()))
3136

3237
subscriber = TCPSubscriber("localhost", 1234, reconnect=False)
3338
with pytest.raises(SubscriptionError):

0 commit comments

Comments
 (0)