Skip to content

Commit 1ba5c73

Browse files
sdb9696rytilahti
andauthored
Fix potential infinite loop if incomplete lists returned (#920)
Fixes the test framework to handle fixtures with incomplete lists better by checking for completeness and overriding the sum. Also adds a pytest-timeout dev dependency with timeout set to 10 seconds. Finally fixes smartprotocol to prevent an infinite loop if incomplete lists ever happens in the real world. Co-authored-by: Teemu R. <tpr@iki.fi>
1 parent 9989d0f commit 1ba5c73

File tree

6 files changed

+105
-5
lines changed

6 files changed

+105
-5
lines changed

kasa/smartprotocol.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ async def _handle_response_lists(
229229
iterate_list_pages=False,
230230
)
231231
next_batch = response[method]
232+
# In case the device returns empty lists avoid infinite looping
233+
if not next_batch[response_list_name]:
234+
_LOGGER.error(
235+
f"Device {self._host} returned empty "
236+
+ f"results list for method {method}"
237+
)
238+
break
232239
response_result[response_list_name].extend(next_batch[response_list_name])
233240

234241
def _handle_response_error_code(self, resp_dict: dict, method, raise_on_error=True):

kasa/tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def pytest_configure():
5858

5959

6060
def pytest_sessionfinish(session, exitstatus):
61+
if not pytest.fixtures_missing_methods:
62+
return
6163
msg = "\n"
6264
for fixture, methods in sorted(pytest.fixtures_missing_methods.items()):
6365
method_list = ", ".join(methods)

kasa/tests/fakeprotocol_smart.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def __init__(
2828
*,
2929
list_return_size=10,
3030
component_nego_not_included=False,
31+
warn_fixture_missing_methods=True,
32+
fix_incomplete_fixture_lists=True,
3133
):
3234
super().__init__(
3335
config=DeviceConfig(
@@ -46,6 +48,8 @@ def __init__(
4648
for comp in self.info["component_nego"]["component_list"]
4749
}
4850
self.list_return_size = list_return_size
51+
self.warn_fixture_missing_methods = warn_fixture_missing_methods
52+
self.fix_incomplete_fixture_lists = fix_incomplete_fixture_lists
4953

5054
@property
5155
def default_port(self):
@@ -220,6 +224,18 @@ def _send_request(self, request_dict: dict):
220224
if (params and (start_index := params.get("start_index")))
221225
else 0
222226
)
227+
# Fixtures generated before _handle_response_lists was implemented
228+
# could have incomplete lists.
229+
if (
230+
len(result[list_key]) < result["sum"]
231+
and self.fix_incomplete_fixture_lists
232+
):
233+
result["sum"] = len(result[list_key])
234+
if self.warn_fixture_missing_methods:
235+
pytest.fixtures_missing_methods.setdefault(
236+
self.fixture_name, set()
237+
).add(f"{method} (incomplete '{list_key}' list)")
238+
223239
result[list_key] = result[list_key][
224240
start_index : start_index + self.list_return_size
225241
]
@@ -244,9 +260,10 @@ def _send_request(self, request_dict: dict):
244260
"method": method,
245261
}
246262
# Reduce warning spam by consolidating and reporting at the end of the run
247-
if self.fixture_name not in pytest.fixtures_missing_methods:
248-
pytest.fixtures_missing_methods[self.fixture_name] = set()
249-
pytest.fixtures_missing_methods[self.fixture_name].add(method)
263+
if self.warn_fixture_missing_methods:
264+
pytest.fixtures_missing_methods.setdefault(
265+
self.fixture_name, set()
266+
).add(method)
250267
return retval
251268
elif method in ["set_qs_info", "fw_download"]:
252269
return {"error_code": 0}

kasa/tests/test_smartprotocol.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
import pytest
24

35
from ..credentials import Credentials
@@ -242,3 +244,49 @@ async def test_smart_protocol_lists_multiple_request(mocker, list_sum, batch_siz
242244
)
243245
assert query_spy.call_count == expected_count
244246
assert resp == response
247+
248+
249+
async def test_incomplete_list(mocker, caplog):
250+
"""Test for handling incomplete lists returned from queries."""
251+
info = {
252+
"get_preset_rules": {
253+
"start_index": 0,
254+
"states": [
255+
{
256+
"brightness": 50,
257+
},
258+
{
259+
"brightness": 100,
260+
},
261+
],
262+
"sum": 7,
263+
}
264+
}
265+
caplog.set_level(logging.ERROR)
266+
transport = FakeSmartTransport(
267+
info,
268+
"dummy-name",
269+
component_nego_not_included=True,
270+
warn_fixture_missing_methods=False,
271+
)
272+
protocol = SmartProtocol(transport=transport)
273+
resp = await protocol.query({"get_preset_rules": None})
274+
assert resp
275+
assert resp["get_preset_rules"]["sum"] == 2 # FakeTransport fixes sum
276+
assert caplog.text == ""
277+
278+
# Test behaviour without FakeTranport fix
279+
transport = FakeSmartTransport(
280+
info,
281+
"dummy-name",
282+
component_nego_not_included=True,
283+
warn_fixture_missing_methods=False,
284+
fix_incomplete_fixture_lists=False,
285+
)
286+
protocol = SmartProtocol(transport=transport)
287+
resp = await protocol.query({"get_preset_rules": None})
288+
assert resp["get_preset_rules"]["sum"] == 7
289+
assert (
290+
"Device 127.0.0.123 returned empty results list for method get_preset_rules"
291+
in caplog.text
292+
)

poetry.lock

Lines changed: 26 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pytest-mock = "*"
5757
codecov = "*"
5858
xdoctest = "*"
5959
coverage = {version = "*", extras = ["toml"]}
60+
pytest-timeout = "^2"
6061

6162
[tool.poetry.extras]
6263
docs = ["sphinx", "sphinx_rtd_theme", "sphinxcontrib-programoutput", "myst-parser", "docutils"]
@@ -89,6 +90,7 @@ markers = [
8990
"requires_dummy: test requires dummy data to pass, skipped on real devices",
9091
]
9192
asyncio_mode = "auto"
93+
timeout = 10
9294

9395
[tool.doc8]
9496
paths = ["docs"]

0 commit comments

Comments
 (0)