Skip to content

Commit aec6937

Browse files
🧪 test: add tests for parse_site_location edge cases (#38)
* test: add tests for parse_site_location edge cases Added unit tests to `tests/unit/test_cli/test_http/test_api/test_site_location.py` to cover `parse_site_location`. The tests cover standard string format, string containing spaces, exceptions handling incorrect lengths (too few or too many parameters), incorrect coordinate formats (non-float characters), and empty strings. Co-authored-by: rnovatorov <[email protected]> * test: fix flaky mqtt adapter tests with events The `test_standalone/test_mqtt_adapter.py` test suite used `await asyncio.sleep(0.02)` to wait for mocked methods to be called before assertions. Under higher load, or in some configurations like Python 3.13, this constant delay was insufficient, causing intermittent failures such as `AssertionError: Expected 'publish_log' to have been called`. This replaces the arbitrary sleeps with deterministic synchronization primitives (`asyncio.Event`) where tests wait explicitly for up to 1 second for the mocked function to be called before checking assertions. Co-authored-by: rnovatorov <[email protected]> * test: fix flaky mqtt adapter tests with events The `test_standalone/test_mqtt_adapter.py` test suite used `await asyncio.sleep(0.02)` to wait for mocked methods to be called before assertions. Under higher load, or in some configurations like Python 3.13, this constant delay was insufficient, causing intermittent failures such as `AssertionError: Expected 'publish_log' to have been called`. This replaces the arbitrary sleeps with deterministic synchronization primitives (`asyncio.Event`) where tests wait explicitly for up to 1 second for the mocked function to be called before checking assertions. Also run `black` to format the new code modifications. Co-authored-by: rnovatorov <[email protected]> * test: fix flaky mqtt adapter tests using asyncio events The `tests/unit/test_standalone/test_mqtt_adapter.py` test suite used `await asyncio.sleep(0.02)` to wait for mocked methods to be called before assertions. Under higher load, or in some configurations, this constant delay was insufficient, causing intermittent failures such as `AssertionError: Expected 'publish_log' to have been called`. This replaces the arbitrary sleeps with deterministic synchronization primitives (`asyncio.Event`) where tests wait explicitly for the mocked function to be called before checking assertions. This commit includes both the code fixes and the `black` formatting applied. Co-authored-by: rnovatorov <[email protected]> * fix: strip whitespace from site location name When parsing site location strings, the `name` is now stripped of extra whitespace to prevent leading and trailing spaces from remaining if passed incorrectly. Updated corresponding unit tests. Co-authored-by: rnovatorov <[email protected]> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 5a4854d commit aec6937

File tree

3 files changed

+92
-10
lines changed

3 files changed

+92
-10
lines changed

‎src/enapter/cli/http/api/site_location.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
def parse_site_location(location_str: str) -> tuple[str, float, float]:
55
try:
66
name, lat_str, lon_str = location_str.split(",")
7-
return name, float(lat_str), float(lon_str)
7+
return name.strip(), float(lat_str), float(lon_str)
88
except ValueError:
99
raise argparse.ArgumentTypeError(
1010
"Location must be in the format NAME,LATITUDE,LONGITUDE"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import argparse
2+
3+
import pytest
4+
5+
from enapter.cli.http.api.site_location import parse_site_location
6+
7+
8+
def test_parse_site_location_valid():
9+
assert parse_site_location("Berlin,52.52,13.405") == ("Berlin", 52.52, 13.405)
10+
11+
12+
def test_parse_site_location_with_spaces():
13+
# Note: name strips whitespace, float() handles surrounding whitespace
14+
assert parse_site_location(" Berlin , 52.52 , 13.405 ") == (
15+
"Berlin",
16+
52.52,
17+
13.405,
18+
)
19+
20+
21+
def test_parse_site_location_too_few_parts():
22+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
23+
parse_site_location("Berlin,52.52")
24+
assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str(
25+
exc_info.value
26+
)
27+
28+
29+
def test_parse_site_location_too_many_parts():
30+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
31+
parse_site_location("Berlin,52.52,13.405,extra")
32+
assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str(
33+
exc_info.value
34+
)
35+
36+
37+
def test_parse_site_location_invalid_latitude():
38+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
39+
parse_site_location("Berlin,invalid,13.405")
40+
assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str(
41+
exc_info.value
42+
)
43+
44+
45+
def test_parse_site_location_invalid_longitude():
46+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
47+
parse_site_location("Berlin,52.52,invalid")
48+
assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str(
49+
exc_info.value
50+
)
51+
52+
53+
def test_parse_site_location_empty():
54+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
55+
parse_site_location("")
56+
assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str(
57+
exc_info.value
58+
)

‎tests/unit/test_standalone/test_mqtt_adapter.py‎

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ async def test_publish_properties():
5050
device = Device()
5151
mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client)
5252
device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel)
53+
event = asyncio.Event()
54+
device_channel.publish_properties.side_effect = lambda *args, **kwargs: event.set()
5355
mqtt_api_client.device_channel.return_value = device_channel
5456
async with asyncio.TaskGroup() as tg:
5557
async with enapter.standalone.mqtt_adapter.MQTTAdapter(
@@ -59,7 +61,7 @@ async def test_publish_properties():
5961
device=device,
6062
task_group=tg,
6163
):
62-
await asyncio.sleep(0.02)
64+
await asyncio.wait_for(event.wait(), timeout=1.0)
6365
device_channel.publish_properties.assert_called()
6466
last_call = device_channel.publish_properties.call_args
6567
published_properties = last_call.kwargs["properties"]
@@ -71,6 +73,8 @@ async def test_publish_telemetry():
7173
device = Device()
7274
mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client)
7375
device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel)
76+
event = asyncio.Event()
77+
device_channel.publish_telemetry.side_effect = lambda *args, **kwargs: event.set()
7478
mqtt_api_client.device_channel.return_value = device_channel
7579
async with asyncio.TaskGroup() as tg:
7680
async with enapter.standalone.mqtt_adapter.MQTTAdapter(
@@ -80,7 +84,7 @@ async def test_publish_telemetry():
8084
device=device,
8185
task_group=tg,
8286
):
83-
await asyncio.sleep(0.02)
87+
await asyncio.wait_for(event.wait(), timeout=1.0)
8488
device_channel.publish_telemetry.assert_called()
8589
last_call = device_channel.publish_telemetry.call_args
8690
published_telemetry = last_call.kwargs["telemetry"]
@@ -94,6 +98,8 @@ async def test_publish_logs(log_severity, persist_logs) -> None:
9498
device = Device(log_severity=log_severity, persist_logs=persist_logs)
9599
mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client)
96100
device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel)
101+
event = asyncio.Event()
102+
device_channel.publish_log.side_effect = lambda *args, **kwargs: event.set()
97103
mqtt_api_client.device_channel.return_value = device_channel
98104
async with asyncio.TaskGroup() as tg:
99105
async with enapter.standalone.mqtt_adapter.MQTTAdapter(
@@ -103,7 +109,7 @@ async def test_publish_logs(log_severity, persist_logs) -> None:
103109
device=device,
104110
task_group=tg,
105111
):
106-
await asyncio.sleep(0.02)
112+
await asyncio.wait_for(event.wait(), timeout=1.0)
107113
device_channel.publish_log.assert_called()
108114
last_call = device_channel.publish_log.call_args
109115
published_log = last_call.kwargs["log"]
@@ -119,7 +125,13 @@ async def test_publish_properties_exception():
119125
device = Device()
120126
mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client)
121127
device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel)
122-
device_channel.publish_properties.side_effect = RuntimeError("Publish error")
128+
event = asyncio.Event()
129+
130+
def publish_properties_mock(*args, **kwargs):
131+
event.set()
132+
raise RuntimeError("Publish error")
133+
134+
device_channel.publish_properties.side_effect = publish_properties_mock
123135
mqtt_api_client.device_channel.return_value = device_channel
124136
async with asyncio.TaskGroup() as tg:
125137
async with enapter.standalone.mqtt_adapter.MQTTAdapter(
@@ -129,15 +141,21 @@ async def test_publish_properties_exception():
129141
device=device,
130142
task_group=tg,
131143
):
132-
await asyncio.sleep(0.02)
144+
await asyncio.wait_for(event.wait(), timeout=1.0)
133145
device_channel.publish_properties.assert_called()
134146

135147

136148
async def test_publish_telemetry_exception():
137149
device = Device()
138150
mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client)
139151
device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel)
140-
device_channel.publish_telemetry.side_effect = RuntimeError("Publish error")
152+
event = asyncio.Event()
153+
154+
def publish_telemetry_mock(*args, **kwargs):
155+
event.set()
156+
raise RuntimeError("Publish error")
157+
158+
device_channel.publish_telemetry.side_effect = publish_telemetry_mock
141159
mqtt_api_client.device_channel.return_value = device_channel
142160
async with asyncio.TaskGroup() as tg:
143161
async with enapter.standalone.mqtt_adapter.MQTTAdapter(
@@ -147,15 +165,21 @@ async def test_publish_telemetry_exception():
147165
device=device,
148166
task_group=tg,
149167
):
150-
await asyncio.sleep(0.02)
168+
await asyncio.wait_for(event.wait(), timeout=1.0)
151169
device_channel.publish_telemetry.assert_called()
152170

153171

154172
async def test_publish_logs_exception():
155173
device = Device(log_severity="error")
156174
mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client)
157175
device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel)
158-
device_channel.publish_log.side_effect = RuntimeError("Publish error")
176+
event = asyncio.Event()
177+
178+
def publish_log_mock(*args, **kwargs):
179+
event.set()
180+
raise RuntimeError("Publish error")
181+
182+
device_channel.publish_log.side_effect = publish_log_mock
159183
mqtt_api_client.device_channel.return_value = device_channel
160184
async with asyncio.TaskGroup() as tg:
161185
async with enapter.standalone.mqtt_adapter.MQTTAdapter(
@@ -165,7 +189,7 @@ async def test_publish_logs_exception():
165189
device=device,
166190
task_group=tg,
167191
):
168-
await asyncio.sleep(0.02)
192+
await asyncio.wait_for(event.wait(), timeout=1.0)
169193
device_channel.publish_log.assert_called()
170194

171195

0 commit comments

Comments
 (0)