From 9b6dea068bdf106da3c98547aa2a4ab9a16a8c90 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:08:52 +0000 Subject: [PATCH 1/5] 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 <20299819+rnovatorov@users.noreply.github.com> --- .../test_http/test_api/test_site_location.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/unit/test_cli/test_http/test_api/test_site_location.py diff --git a/tests/unit/test_cli/test_http/test_api/test_site_location.py b/tests/unit/test_cli/test_http/test_api/test_site_location.py new file mode 100644 index 0000000..afde8e8 --- /dev/null +++ b/tests/unit/test_cli/test_http/test_api/test_site_location.py @@ -0,0 +1,58 @@ +import argparse + +import pytest + +from enapter.cli.http.api.site_location import parse_site_location + + +def test_parse_site_location_valid(): + assert parse_site_location("Berlin,52.52,13.405") == ("Berlin", 52.52, 13.405) + + +def test_parse_site_location_with_spaces(): + # Note: name keeps whitespace, float() handles surrounding whitespace + assert parse_site_location(" Berlin , 52.52 , 13.405 ") == ( + " Berlin ", + 52.52, + 13.405, + ) + + +def test_parse_site_location_too_few_parts(): + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + parse_site_location("Berlin,52.52") + assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str( + exc_info.value + ) + + +def test_parse_site_location_too_many_parts(): + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + parse_site_location("Berlin,52.52,13.405,extra") + assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str( + exc_info.value + ) + + +def test_parse_site_location_invalid_latitude(): + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + parse_site_location("Berlin,invalid,13.405") + assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str( + exc_info.value + ) + + +def test_parse_site_location_invalid_longitude(): + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + parse_site_location("Berlin,52.52,invalid") + assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str( + exc_info.value + ) + + +def test_parse_site_location_empty(): + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + parse_site_location("") + assert "Location must be in the format NAME,LATITUDE,LONGITUDE" in str( + exc_info.value + ) From d856c5ed81ad9119839bc6d4976e21606e03d130 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:13:38 +0000 Subject: [PATCH 2/5] 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 <20299819+rnovatorov@users.noreply.github.com> --- .../unit/test_standalone/test_mqtt_adapter.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_standalone/test_mqtt_adapter.py b/tests/unit/test_standalone/test_mqtt_adapter.py index 53a8004..7b38071 100644 --- a/tests/unit/test_standalone/test_mqtt_adapter.py +++ b/tests/unit/test_standalone/test_mqtt_adapter.py @@ -50,6 +50,8 @@ async def test_publish_properties(): device = Device() mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) + event = asyncio.Event() + device_channel.publish_properties.side_effect = lambda *args, **kwargs: event.set() mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: async with enapter.standalone.mqtt_adapter.MQTTAdapter( @@ -59,7 +61,7 @@ async def test_publish_properties(): device=device, task_group=tg, ): - await asyncio.sleep(0.02) + await asyncio.wait_for(event.wait(), timeout=1.0) device_channel.publish_properties.assert_called() last_call = device_channel.publish_properties.call_args published_properties = last_call.kwargs["properties"] @@ -71,6 +73,8 @@ async def test_publish_telemetry(): device = Device() mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) + event = asyncio.Event() + device_channel.publish_telemetry.side_effect = lambda *args, **kwargs: event.set() mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: async with enapter.standalone.mqtt_adapter.MQTTAdapter( @@ -80,7 +84,7 @@ async def test_publish_telemetry(): device=device, task_group=tg, ): - await asyncio.sleep(0.02) + await asyncio.wait_for(event.wait(), timeout=1.0) device_channel.publish_telemetry.assert_called() last_call = device_channel.publish_telemetry.call_args published_telemetry = last_call.kwargs["telemetry"] @@ -94,6 +98,8 @@ async def test_publish_logs(log_severity, persist_logs) -> None: device = Device(log_severity=log_severity, persist_logs=persist_logs) mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) + event = asyncio.Event() + device_channel.publish_log.side_effect = lambda *args, **kwargs: event.set() mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: async with enapter.standalone.mqtt_adapter.MQTTAdapter( @@ -103,7 +109,7 @@ async def test_publish_logs(log_severity, persist_logs) -> None: device=device, task_group=tg, ): - await asyncio.sleep(0.02) + await asyncio.wait_for(event.wait(), timeout=1.0) device_channel.publish_log.assert_called() last_call = device_channel.publish_log.call_args published_log = last_call.kwargs["log"] @@ -119,7 +125,11 @@ async def test_publish_properties_exception(): device = Device() mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) - device_channel.publish_properties.side_effect = RuntimeError("Publish error") + event = asyncio.Event() + def publish_properties_mock(*args, **kwargs): + event.set() + raise RuntimeError("Publish error") + device_channel.publish_properties.side_effect = publish_properties_mock mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: async with enapter.standalone.mqtt_adapter.MQTTAdapter( @@ -129,7 +139,7 @@ async def test_publish_properties_exception(): device=device, task_group=tg, ): - await asyncio.sleep(0.02) + await asyncio.wait_for(event.wait(), timeout=1.0) device_channel.publish_properties.assert_called() @@ -137,7 +147,11 @@ async def test_publish_telemetry_exception(): device = Device() mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) - device_channel.publish_telemetry.side_effect = RuntimeError("Publish error") + event = asyncio.Event() + def publish_telemetry_mock(*args, **kwargs): + event.set() + raise RuntimeError("Publish error") + device_channel.publish_telemetry.side_effect = publish_telemetry_mock mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: async with enapter.standalone.mqtt_adapter.MQTTAdapter( @@ -147,7 +161,7 @@ async def test_publish_telemetry_exception(): device=device, task_group=tg, ): - await asyncio.sleep(0.02) + await asyncio.wait_for(event.wait(), timeout=1.0) device_channel.publish_telemetry.assert_called() @@ -155,7 +169,11 @@ async def test_publish_logs_exception(): device = Device(log_severity="error") mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) - device_channel.publish_log.side_effect = RuntimeError("Publish error") + event = asyncio.Event() + def publish_log_mock(*args, **kwargs): + event.set() + raise RuntimeError("Publish error") + device_channel.publish_log.side_effect = publish_log_mock mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: async with enapter.standalone.mqtt_adapter.MQTTAdapter( @@ -165,7 +183,7 @@ async def test_publish_logs_exception(): device=device, task_group=tg, ): - await asyncio.sleep(0.02) + await asyncio.wait_for(event.wait(), timeout=1.0) device_channel.publish_log.assert_called() From ab1804f5cb2480eba40e83e528c9f1be59c71e6d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:16:10 +0000 Subject: [PATCH 3/5] 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 <20299819+rnovatorov@users.noreply.github.com> --- tests/unit/test_standalone/test_mqtt_adapter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_standalone/test_mqtt_adapter.py b/tests/unit/test_standalone/test_mqtt_adapter.py index 7b38071..3edbfd1 100644 --- a/tests/unit/test_standalone/test_mqtt_adapter.py +++ b/tests/unit/test_standalone/test_mqtt_adapter.py @@ -126,9 +126,11 @@ async def test_publish_properties_exception(): mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) event = asyncio.Event() + def publish_properties_mock(*args, **kwargs): event.set() raise RuntimeError("Publish error") + device_channel.publish_properties.side_effect = publish_properties_mock mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: @@ -148,9 +150,11 @@ async def test_publish_telemetry_exception(): mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) event = asyncio.Event() + def publish_telemetry_mock(*args, **kwargs): event.set() raise RuntimeError("Publish error") + device_channel.publish_telemetry.side_effect = publish_telemetry_mock mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: @@ -170,9 +174,11 @@ async def test_publish_logs_exception(): mqtt_api_client = mock.AsyncMock(spec=enapter.mqtt.api.Client) device_channel = mock.AsyncMock(spec=enapter.mqtt.api.device.Channel) event = asyncio.Event() + def publish_log_mock(*args, **kwargs): event.set() raise RuntimeError("Publish error") + device_channel.publish_log.side_effect = publish_log_mock mqtt_api_client.device_channel.return_value = device_channel async with asyncio.TaskGroup() as tg: From 5a706cb8f064e440908bd6e86624574efa255ae6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:56:16 +0000 Subject: [PATCH 4/5] 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 <20299819+rnovatorov@users.noreply.github.com> From f260875fe851f127f0e5cafd1b87b6d7ecccb362 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:13:20 +0000 Subject: [PATCH 5/5] 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 <20299819+rnovatorov@users.noreply.github.com> --- src/enapter/cli/http/api/site_location.py | 2 +- tests/unit/test_cli/test_http/test_api/test_site_location.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/enapter/cli/http/api/site_location.py b/src/enapter/cli/http/api/site_location.py index 9504cbf..54a6304 100644 --- a/src/enapter/cli/http/api/site_location.py +++ b/src/enapter/cli/http/api/site_location.py @@ -4,7 +4,7 @@ def parse_site_location(location_str: str) -> tuple[str, float, float]: try: name, lat_str, lon_str = location_str.split(",") - return name, float(lat_str), float(lon_str) + return name.strip(), float(lat_str), float(lon_str) except ValueError: raise argparse.ArgumentTypeError( "Location must be in the format NAME,LATITUDE,LONGITUDE" diff --git a/tests/unit/test_cli/test_http/test_api/test_site_location.py b/tests/unit/test_cli/test_http/test_api/test_site_location.py index afde8e8..b737e0c 100644 --- a/tests/unit/test_cli/test_http/test_api/test_site_location.py +++ b/tests/unit/test_cli/test_http/test_api/test_site_location.py @@ -10,9 +10,9 @@ def test_parse_site_location_valid(): def test_parse_site_location_with_spaces(): - # Note: name keeps whitespace, float() handles surrounding whitespace + # Note: name strips whitespace, float() handles surrounding whitespace assert parse_site_location(" Berlin , 52.52 , 13.405 ") == ( - " Berlin ", + "Berlin", 52.52, 13.405, )