mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Improve testing harness to allow tests on real devices (#197)
* test_cli: provide return values to patched objects to avoid warning about non-awaited calls * test_cli: restore alias after testing * smartstrip: remove internal update() calls for turn_{on,off}, set_led * Make sure power is always a float * Fix discovery tests * Make tests runnable on real devices * Add a note about running tests on a real device * test_strip: run update against the parent device
This commit is contained in:
parent
b088596205
commit
1803a83ae6
@ -94,6 +94,15 @@ This will make sure that the checks are passing when you do a commit.
|
||||
|
||||
You can also execute the checks by running either `tox -e lint` to only do the linting checks, or `tox` to also execute the tests.
|
||||
|
||||
### Running tests
|
||||
|
||||
You can run tests on the library by executing `pytest` in the source directory.
|
||||
This will run the tests against contributed example responses, but you can also execute the tests against a real device:
|
||||
```
|
||||
pytest --ip <address>
|
||||
```
|
||||
Note that this will perform state changes on the device.
|
||||
|
||||
### Analyzing network captures
|
||||
|
||||
The simplest way to add support for a new device or to improve existing ones is to capture traffic between the mobile app and the device.
|
||||
|
@ -247,6 +247,7 @@ async def alias(dev, new_alias, index):
|
||||
if new_alias is not None:
|
||||
click.echo(f"Setting alias to {new_alias}")
|
||||
click.echo(await dev.set_alias(new_alias))
|
||||
await dev.update()
|
||||
|
||||
click.echo(f"Alias: {dev.alias}")
|
||||
if dev.is_strip:
|
||||
|
@ -603,7 +603,7 @@ class SmartDevice:
|
||||
raise SmartDeviceException("Device has no emeter")
|
||||
|
||||
response = EmeterStatus(await self.get_emeter_realtime())
|
||||
return response["power"]
|
||||
return float(response["power"])
|
||||
|
||||
async def reboot(self, delay: int = 1) -> None:
|
||||
"""Reboot the device.
|
||||
@ -658,7 +658,8 @@ class SmartDevice:
|
||||
def device_id(self) -> str:
|
||||
"""Return unique ID for the device.
|
||||
|
||||
This is the MAC address of the device.
|
||||
If not overridden, this is the MAC address of the device.
|
||||
Individual sockets on strips will override this.
|
||||
"""
|
||||
return self.mac
|
||||
|
||||
|
@ -100,12 +100,10 @@ class SmartStrip(SmartDevice):
|
||||
async def turn_on(self, **kwargs):
|
||||
"""Turn the strip on."""
|
||||
await self._query_helper("system", "set_relay_state", {"state": 1})
|
||||
await self.update()
|
||||
|
||||
async def turn_off(self, **kwargs):
|
||||
"""Turn the strip off."""
|
||||
await self._query_helper("system", "set_relay_state", {"state": 0})
|
||||
await self.update()
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@ -126,7 +124,6 @@ class SmartStrip(SmartDevice):
|
||||
async def set_led(self, state: bool):
|
||||
"""Set the state of the led (night mode)."""
|
||||
await self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||
await self.update()
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
|
@ -147,7 +147,7 @@ def get_device_for_file(file):
|
||||
with open(p) as f:
|
||||
sysinfo = json.load(f)
|
||||
model = basename(file)
|
||||
p = device_for_file(model)(host="123.123.123.123")
|
||||
p = device_for_file(model)(host="127.0.0.123")
|
||||
p.protocol = FakeTransportProtocol(sysinfo)
|
||||
asyncio.run(p.update())
|
||||
return p
|
||||
@ -168,21 +168,29 @@ def dev(request):
|
||||
asyncio.run(d.update())
|
||||
if d.model in file:
|
||||
return d
|
||||
raise Exception("Unable to find type for %s" % ip)
|
||||
else:
|
||||
pytest.skip(f"skipping file {file}")
|
||||
|
||||
return get_device_for_file(file)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--ip", action="store", default=None, help="run against device")
|
||||
parser.addoption(
|
||||
"--ip", action="store", default=None, help="run against device on given ip"
|
||||
)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
if not config.getoption("--ip"):
|
||||
print("Testing against fixtures.")
|
||||
return
|
||||
else:
|
||||
print("Running against ip %s" % config.getoption("--ip"))
|
||||
requires_dummy = pytest.mark.skip(
|
||||
reason="test requires to be run against dummy data"
|
||||
)
|
||||
for item in items:
|
||||
if "requires_dummy" in item.keywords:
|
||||
item.add_marker(requires_dummy)
|
||||
|
||||
|
||||
# allow mocks to be awaited
|
||||
|
@ -124,7 +124,7 @@ LIGHT_STATE_SCHEMA = Schema(
|
||||
"dft_on_state": Optional(
|
||||
{
|
||||
"brightness": All(int, Range(min=0, max=100)),
|
||||
"color_temp": All(int, Range(min=2000, max=9000)),
|
||||
"color_temp": All(int, Range(min=0, max=9000)),
|
||||
"hue": All(int, Range(min=0, max=255)),
|
||||
"mode": str,
|
||||
"saturation": All(int, Range(min=0, max=255)),
|
||||
|
@ -66,6 +66,7 @@ async def test_hsv(dev, turn_on):
|
||||
|
||||
await dev.set_hsv(hue=1, saturation=1, value=1)
|
||||
|
||||
await dev.update()
|
||||
hue, saturation, brightness = dev.hsv
|
||||
assert hue == 1
|
||||
assert saturation == 1
|
||||
@ -134,6 +135,7 @@ async def test_variable_temp_state_information(dev):
|
||||
async def test_try_set_colortemp(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
await dev.set_color_temp(2700)
|
||||
await dev.update()
|
||||
assert dev.color_temp == 2700
|
||||
|
||||
|
||||
@ -179,9 +181,11 @@ async def test_dimmable_brightness(dev, turn_on):
|
||||
assert dev.is_dimmable
|
||||
|
||||
await dev.set_brightness(50)
|
||||
await dev.update()
|
||||
assert dev.brightness == 50
|
||||
|
||||
await dev.set_brightness(10)
|
||||
await dev.update()
|
||||
assert dev.brightness == 10
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -18,7 +18,7 @@ async def test_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
runner = CliRunner()
|
||||
res = await runner.invoke(state, obj=dev)
|
||||
print(res.output)
|
||||
await dev.update()
|
||||
|
||||
if dev.is_on:
|
||||
assert "Device state: ON" in res.output
|
||||
@ -32,6 +32,8 @@ async def test_alias(dev):
|
||||
res = await runner.invoke(alias, obj=dev)
|
||||
assert f"Alias: {dev.alias}" in res.output
|
||||
|
||||
old_alias = dev.alias
|
||||
|
||||
new_alias = "new alias"
|
||||
res = await runner.invoke(alias, [new_alias], obj=dev)
|
||||
assert f"Setting alias to {new_alias}" in res.output
|
||||
@ -39,6 +41,8 @@ async def test_alias(dev):
|
||||
res = await runner.invoke(alias, obj=dev)
|
||||
assert f"Alias: {new_alias}" in res.output
|
||||
|
||||
await dev.set_alias(old_alias)
|
||||
|
||||
|
||||
async def test_raw_command(dev):
|
||||
runner = CliRunner()
|
||||
@ -63,11 +67,13 @@ async def test_emeter(dev: SmartDevice, mocker):
|
||||
assert "== Emeter ==" in res.output
|
||||
|
||||
monthly = mocker.patch.object(dev, "get_emeter_monthly")
|
||||
monthly.return_value = []
|
||||
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
|
||||
assert "For year" in res.output
|
||||
monthly.assert_called()
|
||||
|
||||
daily = mocker.patch.object(dev, "get_emeter_daily")
|
||||
daily.return_value = []
|
||||
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
|
||||
assert "For month" in res.output
|
||||
daily.assert_called()
|
||||
|
@ -8,14 +8,14 @@ from .conftest import bulb, dimmer, lightstrip, plug, pytestmark, strip
|
||||
|
||||
@plug
|
||||
async def test_type_detection_plug(dev: SmartDevice):
|
||||
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
assert d.is_plug
|
||||
assert d.device_type == DeviceType.Plug
|
||||
|
||||
|
||||
@bulb
|
||||
async def test_type_detection_bulb(dev: SmartDevice):
|
||||
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
# TODO: light_strip is a special case for now to force bulb tests on it
|
||||
if not d.is_light_strip:
|
||||
assert d.is_bulb
|
||||
@ -24,21 +24,21 @@ async def test_type_detection_bulb(dev: SmartDevice):
|
||||
|
||||
@strip
|
||||
async def test_type_detection_strip(dev: SmartDevice):
|
||||
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
assert d.is_strip
|
||||
assert d.device_type == DeviceType.Strip
|
||||
|
||||
|
||||
@dimmer
|
||||
async def test_type_detection_dimmer(dev: SmartDevice):
|
||||
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
assert d.is_dimmer
|
||||
assert d.device_type == DeviceType.Dimmer
|
||||
|
||||
|
||||
@lightstrip
|
||||
async def test_type_detection_lightstrip(dev: SmartDevice):
|
||||
d = Discover._get_device_class(dev.protocol.discovery_data)("localhost")
|
||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||
assert d.is_light_strip
|
||||
assert d.device_type == DeviceType.LightStrip
|
||||
|
||||
|
@ -32,6 +32,7 @@ async def test_get_emeter_realtime(dev):
|
||||
|
||||
|
||||
@has_emeter
|
||||
@pytest.mark.requires_dummy
|
||||
async def test_get_emeter_daily(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
@ -54,6 +55,7 @@ async def test_get_emeter_daily(dev):
|
||||
|
||||
|
||||
@has_emeter
|
||||
@pytest.mark.requires_dummy
|
||||
async def test_get_emeter_monthly(dev):
|
||||
if dev.is_strip:
|
||||
pytest.skip("Disabled for strips temporarily")
|
||||
|
@ -20,9 +20,11 @@ async def test_led(dev):
|
||||
original = dev.led
|
||||
|
||||
await dev.set_led(False)
|
||||
await dev.update()
|
||||
assert not dev.led
|
||||
|
||||
await dev.set_led(True)
|
||||
await dev.update()
|
||||
assert dev.led
|
||||
|
||||
await dev.set_led(original)
|
||||
|
@ -13,6 +13,7 @@ async def test_state_info(dev):
|
||||
assert isinstance(dev.state_information, dict)
|
||||
|
||||
|
||||
@pytest.mark.requires_dummy
|
||||
async def test_invalid_connection(dev):
|
||||
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
@ -49,18 +50,22 @@ async def test_state(dev, turn_on):
|
||||
orig_state = dev.is_on
|
||||
if orig_state:
|
||||
await dev.turn_off()
|
||||
await dev.update()
|
||||
assert not dev.is_on
|
||||
assert dev.is_off
|
||||
|
||||
await dev.turn_on()
|
||||
await dev.update()
|
||||
assert dev.is_on
|
||||
assert not dev.is_off
|
||||
else:
|
||||
await dev.turn_on()
|
||||
await dev.update()
|
||||
assert dev.is_on
|
||||
assert not dev.is_off
|
||||
|
||||
await dev.turn_off()
|
||||
await dev.update()
|
||||
assert not dev.is_on
|
||||
assert dev.is_off
|
||||
|
||||
@ -71,9 +76,11 @@ async def test_alias(dev):
|
||||
|
||||
assert isinstance(original, str)
|
||||
await dev.set_alias(test_alias)
|
||||
await dev.update()
|
||||
assert dev.alias == test_alias
|
||||
|
||||
await dev.set_alias(original)
|
||||
await dev.update()
|
||||
assert dev.alias == original
|
||||
|
||||
|
||||
|
@ -15,20 +15,24 @@ async def test_children_change_state(dev, turn_on):
|
||||
orig_state = plug.is_on
|
||||
if orig_state:
|
||||
await plug.turn_off()
|
||||
assert not plug.is_on
|
||||
assert plug.is_off
|
||||
await dev.update()
|
||||
assert plug.is_on is False
|
||||
assert plug.is_off is True
|
||||
|
||||
await plug.turn_on()
|
||||
assert plug.is_on
|
||||
assert not plug.is_off
|
||||
await dev.update()
|
||||
assert plug.is_on is True
|
||||
assert plug.is_off is False
|
||||
else:
|
||||
await plug.turn_on()
|
||||
assert plug.is_on
|
||||
assert not plug.is_off
|
||||
await dev.update()
|
||||
assert plug.is_on is True
|
||||
assert plug.is_off is False
|
||||
|
||||
await plug.turn_off()
|
||||
assert not plug.is_on
|
||||
assert plug.is_off
|
||||
await dev.update()
|
||||
assert plug.is_on is False
|
||||
assert plug.is_off is True
|
||||
|
||||
|
||||
@strip
|
||||
|
@ -72,6 +72,11 @@ fail-under = 100
|
||||
exclude = ['kasa/tests/*']
|
||||
verbose = 2
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"requires_dummy: test requires dummy data to pass, skipped on real devices",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
Loading…
Reference in New Issue
Block a user