Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion airos/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
parsed_data: dict[str, Any] | None = self.parse_airos_packet(data, host_ip)
if parsed_data:
# Schedule the user-provided callback, don't await to keep listener responsive
asyncio.create_task(self.callback(parsed_data)) # noqa: RUF006
self.callback(parsed_data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Async callback not properly invoked.

The callback is documented as asynchronous (line 36) but is now called directly without awaiting, causing the coroutine to never execute. The pipeline failure confirms this with "RuntimeWarning: coroutine was never awaited."

Choose one of these solutions:

Option 1: Restore async task scheduling (recommended)

-                self.callback(parsed_data)
+                asyncio.create_task(self.callback(parsed_data))

Option 2: Await the callback (blocks the listener)

-                self.callback(parsed_data)
+                await self.callback(parsed_data)

Option 3: Make callback synchronous (breaking change)

-    def __init__(self, callback: Callable[[dict[str, Any]], None]) -> None:
+    def __init__(self, callback: Callable[[dict[str, Any]], None]) -> None:
         """Initialize AirosDiscoveryProtocol.

         Args:
-            callback: An asynchronous function to call when a device is discovered.
+            callback: A synchronous function to call when a device is discovered.
                       It should accept a dictionary containing device information.

The first option maintains the original responsive behavior while properly handling async callbacks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.callback(parsed_data)
- self.callback(parsed_data)
+ asyncio.create_task(self.callback(parsed_data))
🧰 Tools
🪛 GitHub Actions: Latest commit

[warning] 62-62: RuntimeWarning: coroutine 'AsyncMockMixin._execute_mock_call' was never awaited. This warning indicates a coroutine was not awaited properly.

🤖 Prompt for AI Agents
In airos/discovery.py at line 62, the async callback is called directly without
awaiting, causing the coroutine to never execute. To fix this, restore the async
task scheduling by using an appropriate method like asyncio.create_task to
schedule the callback instead of calling it directly. This ensures the callback
runs asynchronously without blocking the listener.

except (AirosEndpointError, AirosListenerError) as err:
# These are expected types of malformed packets. Log the specific error
# and then re-raise as AirosDiscoveryError.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "airos"
version = "0.2.0"
version = "0.2.1a1"
license = "MIT"
description = "Ubiquity airOS module(s) for Python 3."
readme = "README.md"
Expand Down
62 changes: 50 additions & 12 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
from airos.exceptions import AirosDiscoveryError, AirosEndpointError
import pytest

expected_parsed_data_for_mock_packet = {
"ip_address": "192.168.1.3", # This comes from the host_ip passed to datagram_received
"mac_address": "01:23:45:67:89:CD", # From 0x06 TLV
"hostname": "name", # From 0x0B TLV
"model": "NanoStation 5AC loco", # From 0x0C TLV
"firmware_version": "WA.V8.7.17", # From 0x03 TLV
"uptime_seconds": 3231, # From 0x0A TLV (0x0C9F in hex)
"ssid": "DemoSSID", # From 0x0D TLV
"full_model_name": "NanoStation 5AC loco", # From 0x14 TLV
}
Comment on lines +12 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify uptime_seconds value consistency.

There's a discrepancy in the uptime_seconds value. The new dictionary shows 3231 (0x0C9F), but the existing test_parse_airos_packet_success expects 265375. This inconsistency could indicate either incorrect test data or different fixtures being used.


🏁 Script executed:

#!/bin/bash
# Description: Check the actual uptime value in the binary fixture
# Expected: Verify which uptime value is correct by examining the fixture

# Look for the fixture file and examine its contents
fd -t f "airos_sta_discovery_packet.bin" fixtures/
if [ $? -eq 0 ]; then
    echo "Found fixture file. Checking TLV 0x0A (uptime) content..."
    # Use hexdump to examine the binary content
    hexdump -C fixtures/airos_sta_discovery_packet.bin | grep -A5 -B5 "0a"
fi

Length of output: 403


I couldn’t inspect the TLV parsing logic with hexdump. Let’s locate and review the packet‐parsing implementation to understand how the uptime field is decoded and then re‐extract it from the fixture.


🏁 Script executed:

#!/bin/bash
# 1) Find and show the parse_airos_packet function with context
rg -n "def parse_airos_packet" -tpy
rg -A20 -B5 "def parse_airos_packet" -tpy

# 2) Within that function (or nearby), look for handling of TLV type 0x0A / "uptime"
rg -n "0x0A" -C5 -tpy
rg -n "uptime" -C5 -tpy

Length of output: 9942


Update expected uptime_seconds to match the 4‐byte unpacked value

The mock‐packet dict in tests/test_discovery.py currently uses 3231 (0x0C9F), but the fixture generator packs a 4-byte big-endian integer (UPTIME_SECONDS = 265375, i.e. 0x00040C9F), and the existing test_parse_airos_packet_success asserts 265375. To keep the tests consistent, change:

 tests/test_discovery.py
 @@ Lines 12–21
-    "uptime_seconds": 3231,  # From 0x0A TLV (0x0C9F in hex)
+    "uptime_seconds": 265375,  # From 0x0A TLV (0x00040C9F in hex)

• Location: tests/test_discovery.py, around line 18.
• Ensures both test_datagram_received_calls_callback and test_parse_airos_packet_success align on the same unpacked uptime value.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expected_parsed_data_for_mock_packet = {
"ip_address": "192.168.1.3", # This comes from the host_ip passed to datagram_received
"mac_address": "01:23:45:67:89:CD", # From 0x06 TLV
"hostname": "name", # From 0x0B TLV
"model": "NanoStation 5AC loco", # From 0x0C TLV
"firmware_version": "WA.V8.7.17", # From 0x03 TLV
"uptime_seconds": 3231, # From 0x0A TLV (0x0C9F in hex)
"ssid": "DemoSSID", # From 0x0D TLV
"full_model_name": "NanoStation 5AC loco", # From 0x14 TLV
}
# tests/test_discovery.py
expected_parsed_data_for_mock_packet = {
"ip_address": "192.168.1.3", # This comes from the host_ip passed to datagram_received
"mac_address": "01:23:45:67:89:CD", # From 0x06 TLV
"hostname": "name", # From 0x0B TLV
"model": "NanoStation 5AC loco", # From 0x0C TLV
"firmware_version": "WA.V8.7.17", # From 0x03 TLV
"uptime_seconds": 265375, # From 0x0A TLV (0x00040C9F in hex)
"ssid": "DemoSSID", # From 0x0D TLV
"full_model_name": "NanoStation 5AC loco", # From 0x14 TLV
}
🤖 Prompt for AI Agents
In tests/test_discovery.py around line 18, update the expected value of
"uptime_seconds" in the mock packet dictionary from 3231 to 265375 to match the
4-byte big-endian integer unpacked value used in the fixture generator. This
change ensures consistency between the test_datagram_received_calls_callback and
test_parse_airos_packet_success tests.



# Helper to load binary fixture
async def _read_binary_fixture(fixture_name: str) -> bytes:
Expand Down Expand Up @@ -119,21 +130,48 @@ async def test_datagram_received_calls_callback(mock_airos_packet):
protocol = AirosDiscoveryProtocol(mock_callback)
host_ip = "192.168.1.3" # Sender IP

with patch("asyncio.create_task") as mock_create_task:
protocol.datagram_received(mock_airos_packet, (host_ip, DISCOVERY_PORT))
protocol.datagram_received(mock_airos_packet, (host_ip, DISCOVERY_PORT))

# Verify the task was created and get the coroutine
mock_create_task.assert_called_once()
task_coro = mock_create_task.call_args[0][0]
# Assert that the mock_callback was called exactly once
mock_callback.assert_called_once()

# Manually await the coroutine to test the callback
await task_coro
# Get the arguments it was called with
args, kwargs = mock_callback.call_args

mock_callback.assert_called_once()
called_args, _ = mock_callback.call_args
parsed_data = called_args[0]
assert parsed_data["ip_address"] == "192.168.1.3"
assert parsed_data["mac_address"] == "01:23:45:67:89:CD" # Verify scrubbed MAC
# Assert that the callback was called with a single argument (the parsed data)
assert len(args) == 1
assert not kwargs # No keyword arguments expected

# Get the parsed data from the call
actual_parsed_data = args[0]

# Assert that the parsed data matches the expected structure and values
# We need to make sure the IP address in the expected data reflects the host_ip
expected_parsed_data_with_current_ip = expected_parsed_data_for_mock_packet.copy()
expected_parsed_data_with_current_ip["ip_address"] = host_ip

# For robust testing, you might want to only check the critical fields,
# or ensure your `parse_airos_packet` is separately tested to be correct.
# Here, we'll assert a subset of key fields for simplicity.
assert (
actual_parsed_data.get("mac_address")
== expected_parsed_data_with_current_ip["mac_address"]
)
assert (
actual_parsed_data.get("hostname")
== expected_parsed_data_with_current_ip["hostname"]
)
assert (
actual_parsed_data.get("ip_address")
== expected_parsed_data_with_current_ip["ip_address"]
)
assert (
actual_parsed_data.get("model") == expected_parsed_data_with_current_ip["model"]
)
assert (
actual_parsed_data.get("firmware_version")
== expected_parsed_data_with_current_ip["firmware_version"]
)


@pytest.mark.asyncio
Expand Down
Loading