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.
|
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
|
### 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.
|
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:
|
if new_alias is not None:
|
||||||
click.echo(f"Setting alias to {new_alias}")
|
click.echo(f"Setting alias to {new_alias}")
|
||||||
click.echo(await dev.set_alias(new_alias))
|
click.echo(await dev.set_alias(new_alias))
|
||||||
|
await dev.update()
|
||||||
|
|
||||||
click.echo(f"Alias: {dev.alias}")
|
click.echo(f"Alias: {dev.alias}")
|
||||||
if dev.is_strip:
|
if dev.is_strip:
|
||||||
|
@ -603,7 +603,7 @@ class SmartDevice:
|
|||||||
raise SmartDeviceException("Device has no emeter")
|
raise SmartDeviceException("Device has no emeter")
|
||||||
|
|
||||||
response = EmeterStatus(await self.get_emeter_realtime())
|
response = EmeterStatus(await self.get_emeter_realtime())
|
||||||
return response["power"]
|
return float(response["power"])
|
||||||
|
|
||||||
async def reboot(self, delay: int = 1) -> None:
|
async def reboot(self, delay: int = 1) -> None:
|
||||||
"""Reboot the device.
|
"""Reboot the device.
|
||||||
@ -658,7 +658,8 @@ class SmartDevice:
|
|||||||
def device_id(self) -> str:
|
def device_id(self) -> str:
|
||||||
"""Return unique ID for the device.
|
"""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
|
return self.mac
|
||||||
|
|
||||||
|
@ -100,12 +100,10 @@ class SmartStrip(SmartDevice):
|
|||||||
async def turn_on(self, **kwargs):
|
async def turn_on(self, **kwargs):
|
||||||
"""Turn the strip on."""
|
"""Turn the strip on."""
|
||||||
await self._query_helper("system", "set_relay_state", {"state": 1})
|
await self._query_helper("system", "set_relay_state", {"state": 1})
|
||||||
await self.update()
|
|
||||||
|
|
||||||
async def turn_off(self, **kwargs):
|
async def turn_off(self, **kwargs):
|
||||||
"""Turn the strip off."""
|
"""Turn the strip off."""
|
||||||
await self._query_helper("system", "set_relay_state", {"state": 0})
|
await self._query_helper("system", "set_relay_state", {"state": 0})
|
||||||
await self.update()
|
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -126,7 +124,6 @@ class SmartStrip(SmartDevice):
|
|||||||
async def set_led(self, state: bool):
|
async def set_led(self, state: bool):
|
||||||
"""Set the state of the led (night mode)."""
|
"""Set the state of the led (night mode)."""
|
||||||
await self._query_helper("system", "set_led_off", {"off": int(not state)})
|
await self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||||
await self.update()
|
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
|
@ -147,7 +147,7 @@ def get_device_for_file(file):
|
|||||||
with open(p) as f:
|
with open(p) as f:
|
||||||
sysinfo = json.load(f)
|
sysinfo = json.load(f)
|
||||||
model = basename(file)
|
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)
|
p.protocol = FakeTransportProtocol(sysinfo)
|
||||||
asyncio.run(p.update())
|
asyncio.run(p.update())
|
||||||
return p
|
return p
|
||||||
@ -168,21 +168,29 @@ def dev(request):
|
|||||||
asyncio.run(d.update())
|
asyncio.run(d.update())
|
||||||
if d.model in file:
|
if d.model in file:
|
||||||
return d
|
return d
|
||||||
raise Exception("Unable to find type for %s" % ip)
|
else:
|
||||||
|
pytest.skip(f"skipping file {file}")
|
||||||
|
|
||||||
return get_device_for_file(file)
|
return get_device_for_file(file)
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
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):
|
def pytest_collection_modifyitems(config, items):
|
||||||
if not config.getoption("--ip"):
|
if not config.getoption("--ip"):
|
||||||
print("Testing against fixtures.")
|
print("Testing against fixtures.")
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
print("Running against ip %s" % config.getoption("--ip"))
|
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
|
# allow mocks to be awaited
|
||||||
|
@ -124,7 +124,7 @@ LIGHT_STATE_SCHEMA = Schema(
|
|||||||
"dft_on_state": Optional(
|
"dft_on_state": Optional(
|
||||||
{
|
{
|
||||||
"brightness": All(int, Range(min=0, max=100)),
|
"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)),
|
"hue": All(int, Range(min=0, max=255)),
|
||||||
"mode": str,
|
"mode": str,
|
||||||
"saturation": All(int, Range(min=0, max=255)),
|
"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.set_hsv(hue=1, saturation=1, value=1)
|
||||||
|
|
||||||
|
await dev.update()
|
||||||
hue, saturation, brightness = dev.hsv
|
hue, saturation, brightness = dev.hsv
|
||||||
assert hue == 1
|
assert hue == 1
|
||||||
assert saturation == 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):
|
async def test_try_set_colortemp(dev, turn_on):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
await dev.set_color_temp(2700)
|
await dev.set_color_temp(2700)
|
||||||
|
await dev.update()
|
||||||
assert dev.color_temp == 2700
|
assert dev.color_temp == 2700
|
||||||
|
|
||||||
|
|
||||||
@ -179,9 +181,11 @@ async def test_dimmable_brightness(dev, turn_on):
|
|||||||
assert dev.is_dimmable
|
assert dev.is_dimmable
|
||||||
|
|
||||||
await dev.set_brightness(50)
|
await dev.set_brightness(50)
|
||||||
|
await dev.update()
|
||||||
assert dev.brightness == 50
|
assert dev.brightness == 50
|
||||||
|
|
||||||
await dev.set_brightness(10)
|
await dev.set_brightness(10)
|
||||||
|
await dev.update()
|
||||||
assert dev.brightness == 10
|
assert dev.brightness == 10
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -18,7 +18,7 @@ async def test_state(dev, turn_on):
|
|||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
res = await runner.invoke(state, obj=dev)
|
res = await runner.invoke(state, obj=dev)
|
||||||
print(res.output)
|
await dev.update()
|
||||||
|
|
||||||
if dev.is_on:
|
if dev.is_on:
|
||||||
assert "Device state: ON" in res.output
|
assert "Device state: ON" in res.output
|
||||||
@ -32,6 +32,8 @@ async def test_alias(dev):
|
|||||||
res = await runner.invoke(alias, obj=dev)
|
res = await runner.invoke(alias, obj=dev)
|
||||||
assert f"Alias: {dev.alias}" in res.output
|
assert f"Alias: {dev.alias}" in res.output
|
||||||
|
|
||||||
|
old_alias = dev.alias
|
||||||
|
|
||||||
new_alias = "new alias"
|
new_alias = "new alias"
|
||||||
res = await runner.invoke(alias, [new_alias], obj=dev)
|
res = await runner.invoke(alias, [new_alias], obj=dev)
|
||||||
assert f"Setting alias to {new_alias}" in res.output
|
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)
|
res = await runner.invoke(alias, obj=dev)
|
||||||
assert f"Alias: {new_alias}" in res.output
|
assert f"Alias: {new_alias}" in res.output
|
||||||
|
|
||||||
|
await dev.set_alias(old_alias)
|
||||||
|
|
||||||
|
|
||||||
async def test_raw_command(dev):
|
async def test_raw_command(dev):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
@ -63,11 +67,13 @@ async def test_emeter(dev: SmartDevice, mocker):
|
|||||||
assert "== Emeter ==" in res.output
|
assert "== Emeter ==" in res.output
|
||||||
|
|
||||||
monthly = mocker.patch.object(dev, "get_emeter_monthly")
|
monthly = mocker.patch.object(dev, "get_emeter_monthly")
|
||||||
|
monthly.return_value = []
|
||||||
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
|
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
|
||||||
assert "For year" in res.output
|
assert "For year" in res.output
|
||||||
monthly.assert_called()
|
monthly.assert_called()
|
||||||
|
|
||||||
daily = mocker.patch.object(dev, "get_emeter_daily")
|
daily = mocker.patch.object(dev, "get_emeter_daily")
|
||||||
|
daily.return_value = []
|
||||||
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
|
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
|
||||||
assert "For month" in res.output
|
assert "For month" in res.output
|
||||||
daily.assert_called()
|
daily.assert_called()
|
||||||
|
@ -8,14 +8,14 @@ from .conftest import bulb, dimmer, lightstrip, plug, pytestmark, strip
|
|||||||
|
|
||||||
@plug
|
@plug
|
||||||
async def test_type_detection_plug(dev: SmartDevice):
|
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.is_plug
|
||||||
assert d.device_type == DeviceType.Plug
|
assert d.device_type == DeviceType.Plug
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb
|
||||||
async def test_type_detection_bulb(dev: SmartDevice):
|
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
|
# TODO: light_strip is a special case for now to force bulb tests on it
|
||||||
if not d.is_light_strip:
|
if not d.is_light_strip:
|
||||||
assert d.is_bulb
|
assert d.is_bulb
|
||||||
@ -24,21 +24,21 @@ async def test_type_detection_bulb(dev: SmartDevice):
|
|||||||
|
|
||||||
@strip
|
@strip
|
||||||
async def test_type_detection_strip(dev: SmartDevice):
|
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.is_strip
|
||||||
assert d.device_type == DeviceType.Strip
|
assert d.device_type == DeviceType.Strip
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer
|
||||||
async def test_type_detection_dimmer(dev: SmartDevice):
|
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.is_dimmer
|
||||||
assert d.device_type == DeviceType.Dimmer
|
assert d.device_type == DeviceType.Dimmer
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip
|
||||||
async def test_type_detection_lightstrip(dev: SmartDevice):
|
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.is_light_strip
|
||||||
assert d.device_type == DeviceType.LightStrip
|
assert d.device_type == DeviceType.LightStrip
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ async def test_get_emeter_realtime(dev):
|
|||||||
|
|
||||||
|
|
||||||
@has_emeter
|
@has_emeter
|
||||||
|
@pytest.mark.requires_dummy
|
||||||
async def test_get_emeter_daily(dev):
|
async def test_get_emeter_daily(dev):
|
||||||
if dev.is_strip:
|
if dev.is_strip:
|
||||||
pytest.skip("Disabled for strips temporarily")
|
pytest.skip("Disabled for strips temporarily")
|
||||||
@ -54,6 +55,7 @@ async def test_get_emeter_daily(dev):
|
|||||||
|
|
||||||
|
|
||||||
@has_emeter
|
@has_emeter
|
||||||
|
@pytest.mark.requires_dummy
|
||||||
async def test_get_emeter_monthly(dev):
|
async def test_get_emeter_monthly(dev):
|
||||||
if dev.is_strip:
|
if dev.is_strip:
|
||||||
pytest.skip("Disabled for strips temporarily")
|
pytest.skip("Disabled for strips temporarily")
|
||||||
|
@ -20,9 +20,11 @@ async def test_led(dev):
|
|||||||
original = dev.led
|
original = dev.led
|
||||||
|
|
||||||
await dev.set_led(False)
|
await dev.set_led(False)
|
||||||
|
await dev.update()
|
||||||
assert not dev.led
|
assert not dev.led
|
||||||
|
|
||||||
await dev.set_led(True)
|
await dev.set_led(True)
|
||||||
|
await dev.update()
|
||||||
assert dev.led
|
assert dev.led
|
||||||
|
|
||||||
await dev.set_led(original)
|
await dev.set_led(original)
|
||||||
|
@ -13,6 +13,7 @@ async def test_state_info(dev):
|
|||||||
assert isinstance(dev.state_information, dict)
|
assert isinstance(dev.state_information, dict)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.requires_dummy
|
||||||
async def test_invalid_connection(dev):
|
async def test_invalid_connection(dev):
|
||||||
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
||||||
with pytest.raises(SmartDeviceException):
|
with pytest.raises(SmartDeviceException):
|
||||||
@ -49,18 +50,22 @@ async def test_state(dev, turn_on):
|
|||||||
orig_state = dev.is_on
|
orig_state = dev.is_on
|
||||||
if orig_state:
|
if orig_state:
|
||||||
await dev.turn_off()
|
await dev.turn_off()
|
||||||
|
await dev.update()
|
||||||
assert not dev.is_on
|
assert not dev.is_on
|
||||||
assert dev.is_off
|
assert dev.is_off
|
||||||
|
|
||||||
await dev.turn_on()
|
await dev.turn_on()
|
||||||
|
await dev.update()
|
||||||
assert dev.is_on
|
assert dev.is_on
|
||||||
assert not dev.is_off
|
assert not dev.is_off
|
||||||
else:
|
else:
|
||||||
await dev.turn_on()
|
await dev.turn_on()
|
||||||
|
await dev.update()
|
||||||
assert dev.is_on
|
assert dev.is_on
|
||||||
assert not dev.is_off
|
assert not dev.is_off
|
||||||
|
|
||||||
await dev.turn_off()
|
await dev.turn_off()
|
||||||
|
await dev.update()
|
||||||
assert not dev.is_on
|
assert not dev.is_on
|
||||||
assert dev.is_off
|
assert dev.is_off
|
||||||
|
|
||||||
@ -71,9 +76,11 @@ async def test_alias(dev):
|
|||||||
|
|
||||||
assert isinstance(original, str)
|
assert isinstance(original, str)
|
||||||
await dev.set_alias(test_alias)
|
await dev.set_alias(test_alias)
|
||||||
|
await dev.update()
|
||||||
assert dev.alias == test_alias
|
assert dev.alias == test_alias
|
||||||
|
|
||||||
await dev.set_alias(original)
|
await dev.set_alias(original)
|
||||||
|
await dev.update()
|
||||||
assert dev.alias == original
|
assert dev.alias == original
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,20 +15,24 @@ async def test_children_change_state(dev, turn_on):
|
|||||||
orig_state = plug.is_on
|
orig_state = plug.is_on
|
||||||
if orig_state:
|
if orig_state:
|
||||||
await plug.turn_off()
|
await plug.turn_off()
|
||||||
assert not plug.is_on
|
await dev.update()
|
||||||
assert plug.is_off
|
assert plug.is_on is False
|
||||||
|
assert plug.is_off is True
|
||||||
|
|
||||||
await plug.turn_on()
|
await plug.turn_on()
|
||||||
assert plug.is_on
|
await dev.update()
|
||||||
assert not plug.is_off
|
assert plug.is_on is True
|
||||||
|
assert plug.is_off is False
|
||||||
else:
|
else:
|
||||||
await plug.turn_on()
|
await plug.turn_on()
|
||||||
assert plug.is_on
|
await dev.update()
|
||||||
assert not plug.is_off
|
assert plug.is_on is True
|
||||||
|
assert plug.is_off is False
|
||||||
|
|
||||||
await plug.turn_off()
|
await plug.turn_off()
|
||||||
assert not plug.is_on
|
await dev.update()
|
||||||
assert plug.is_off
|
assert plug.is_on is False
|
||||||
|
assert plug.is_off is True
|
||||||
|
|
||||||
|
|
||||||
@strip
|
@strip
|
||||||
|
@ -72,6 +72,11 @@ fail-under = 100
|
|||||||
exclude = ['kasa/tests/*']
|
exclude = ['kasa/tests/*']
|
||||||
verbose = 2
|
verbose = 2
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
markers = [
|
||||||
|
"requires_dummy: test requires dummy data to pass, skipped on real devices",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.masonry.api"
|
||||||
|
Loading…
Reference in New Issue
Block a user