From 1803a83ae628f7db3b2c76582dd26cd6f3c029d4 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 19 Sep 2021 23:45:48 +0200 Subject: [PATCH] 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 --- README.md | 9 +++++++++ kasa/cli.py | 1 + kasa/smartdevice.py | 5 +++-- kasa/smartstrip.py | 3 --- kasa/tests/conftest.py | 16 ++++++++++++---- kasa/tests/newfakes.py | 2 +- kasa/tests/test_bulb.py | 4 ++++ kasa/tests/test_cli.py | 8 +++++++- kasa/tests/test_discovery.py | 10 +++++----- kasa/tests/test_emeter.py | 2 ++ kasa/tests/test_plug.py | 2 ++ kasa/tests/test_smartdevice.py | 7 +++++++ kasa/tests/test_strip.py | 20 ++++++++++++-------- pyproject.toml | 5 +++++ 14 files changed, 70 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9cb1b3c6..f8aa8250 100644 --- a/README.md +++ b/README.md @@ -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
+``` +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. diff --git a/kasa/cli.py b/kasa/cli.py index f9f17bce..626eadc2 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -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: diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index cfca4c44..0a722c5d 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -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 diff --git a/kasa/smartstrip.py b/kasa/smartstrip.py index 9c8064dd..a052c0d0 100755 --- a/kasa/smartstrip.py +++ b/kasa/smartstrip.py @@ -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 diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index c3cb683f..857aa373 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -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 diff --git a/kasa/tests/newfakes.py b/kasa/tests/newfakes.py index 1e053621..02c8cdde 100644 --- a/kasa/tests/newfakes.py +++ b/kasa/tests/newfakes.py @@ -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)), diff --git a/kasa/tests/test_bulb.py b/kasa/tests/test_bulb.py index 7d6e45e0..c7beb1ab 100644 --- a/kasa/tests/test_bulb.py +++ b/kasa/tests/test_bulb.py @@ -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): diff --git a/kasa/tests/test_cli.py b/kasa/tests/test_cli.py index 1b94d489..864adb21 100644 --- a/kasa/tests/test_cli.py +++ b/kasa/tests/test_cli.py @@ -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() diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py index 529ad8d6..1356892e 100644 --- a/kasa/tests/test_discovery.py +++ b/kasa/tests/test_discovery.py @@ -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 diff --git a/kasa/tests/test_emeter.py b/kasa/tests/test_emeter.py index 75afe7dd..388a42d4 100644 --- a/kasa/tests/test_emeter.py +++ b/kasa/tests/test_emeter.py @@ -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") diff --git a/kasa/tests/test_plug.py b/kasa/tests/test_plug.py index a301095e..3de3a146 100644 --- a/kasa/tests/test_plug.py +++ b/kasa/tests/test_plug.py @@ -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) diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index 622a2126..002adb90 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -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 diff --git a/kasa/tests/test_strip.py b/kasa/tests/test_strip.py index 861b56ed..21a11e37 100644 --- a/kasa/tests/test_strip.py +++ b/kasa/tests/test_strip.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index f3560599..c87dbd96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"