From 3e25c79f3965b1504aa5ba950e0ffb5455179981 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Thu, 15 Feb 2024 15:12:51 +0100 Subject: [PATCH] Fix tests --- kasa/cli.py | 2 +- kasa/tests/test_cli.py | 22 ++++++++++ kasa/tests/test_feature.py | 79 ++++++++++++++++++++++++++++++++++ kasa/tests/test_smartdevice.py | 8 ++-- 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 kasa/tests/test_feature.py diff --git a/kasa/cli.py b/kasa/cli.py index 037abae0..9d4991aa 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -1117,7 +1117,7 @@ async def feature(dev, name: str, value): If both *name* and *value* are set, the described setting is changed. """ if not name: - echo("[bold]== Feature ==[/bold]") + echo("[bold]== Features ==[/bold]") for name, feat in dev.features.items(): echo(f"{feat.name} ({name}): {feat.value}") return diff --git a/kasa/tests/test_cli.py b/kasa/tests/test_cli.py index 84f016c0..f44a924a 100644 --- a/kasa/tests/test_cli.py +++ b/kasa/tests/test_cli.py @@ -36,6 +36,11 @@ async def test_update_called_by_cli(dev, mocker): """Test that device update is called on main.""" runner = CliRunner() update = mocker.patch.object(dev, "update") + + # These will mock the features to avoid accessing non-existing + mocker.patch("kasa.device.Device.features", return_value={}) + mocker.patch("kasa.iot.iotdevice.IotDevice.features", return_value={}) + mocker.patch("kasa.discover.Discover.discover_single", return_value=dev) res = await runner.invoke( @@ -48,6 +53,7 @@ async def test_update_called_by_cli(dev, mocker): "--password", "bar", ], + catch_exceptions=False, ) assert res.exit_code == 0 update.assert_called() @@ -291,6 +297,10 @@ async def test_brightness(dev): async def test_json_output(dev: Device, mocker): """Test that the json output produces correct output.""" mocker.patch("kasa.Discover.discover", return_value={"127.0.0.1": dev}) + # These will mock the features to avoid accessing non-existing + mocker.patch("kasa.device.Device.features", return_value={}) + mocker.patch("kasa.iot.iotdevice.IotDevice.features", return_value={}) + runner = CliRunner() res = await runner.invoke(cli, ["--json", "state"], obj=dev) assert res.exit_code == 0 @@ -342,6 +352,10 @@ async def test_without_device_type(dev, mocker): """Test connecting without the device type.""" runner = CliRunner() mocker.patch("kasa.discover.Discover.discover_single", return_value=dev) + # These will mock the features to avoid accessing non-existing + mocker.patch("kasa.device.Device.features", return_value={}) + mocker.patch("kasa.iot.iotdevice.IotDevice.features", return_value={}) + res = await runner.invoke( cli, [ @@ -398,6 +412,10 @@ async def test_duplicate_target_device(): async def test_discover(discovery_mock, mocker): """Test discovery output.""" + # These will mock the features to avoid accessing non-existing + mocker.patch("kasa.device.Device.features", return_value={}) + mocker.patch("kasa.iot.iotdevice.IotDevice.features", return_value={}) + runner = CliRunner() res = await runner.invoke( cli, @@ -417,6 +435,10 @@ async def test_discover(discovery_mock, mocker): async def test_discover_host(discovery_mock, mocker): """Test discovery output.""" + # These will mock the features to avoid accessing non-existing + mocker.patch("kasa.device.Device.features", return_value={}) + mocker.patch("kasa.iot.iotdevice.IotDevice.features", return_value={}) + runner = CliRunner() res = await runner.invoke( cli, diff --git a/kasa/tests/test_feature.py b/kasa/tests/test_feature.py new file mode 100644 index 00000000..549f4266 --- /dev/null +++ b/kasa/tests/test_feature.py @@ -0,0 +1,79 @@ +import pytest + +from kasa import Feature, FeatureType + + +@pytest.fixture +def dummy_feature() -> Feature: + # create_autospec for device slows tests way too much, so we use a dummy here + class DummyDevice: + pass + + feat = Feature( + device=DummyDevice(), # type: ignore[arg-type] + name="dummy_feature", + attribute_getter="dummygetter", + attribute_setter="dummysetter", + container=None, + icon="mdi:dummy", + type=FeatureType.BinarySensor, + ) + return feat + + +def test_feature_api(dummy_feature: Feature): + """Test all properties of a dummy feature.""" + assert dummy_feature.device is not None + assert dummy_feature.name == "dummy_feature" + assert dummy_feature.attribute_getter == "dummygetter" + assert dummy_feature.attribute_setter == "dummysetter" + assert dummy_feature.container is None + assert dummy_feature.icon == "mdi:dummy" + assert dummy_feature.type == FeatureType.BinarySensor + + +def test_feature_value(dummy_feature: Feature): + """Verify that property gets accessed on *value* access.""" + dummy_feature.attribute_getter = "test_prop" + dummy_feature.device.test_prop = "dummy" # type: ignore[attr-defined] + assert dummy_feature.value == "dummy" + + +def test_feature_value_container(mocker, dummy_feature: Feature): + """Test that container's attribute is accessed when expected.""" + + class DummyContainer: + @property + def test_prop(self): + return "dummy" + + dummy_feature.container = DummyContainer() + dummy_feature.attribute_getter = "test_prop" + + mock_dev_prop = mocker.patch.object( + dummy_feature, "test_prop", new_callable=mocker.PropertyMock, create=True + ) + + assert dummy_feature.value == "dummy" + mock_dev_prop.assert_not_called() + + +def test_feature_value_callable(dev, dummy_feature: Feature): + """Verify that callables work as *attribute_getter*.""" + dummy_feature.attribute_getter = lambda x: "dummy value" + assert dummy_feature.value == "dummy value" + + +async def test_feature_setter(dev, mocker, dummy_feature: Feature): + """Verify that *set_value* calls the defined method.""" + mock_set_dummy = mocker.patch.object(dummy_feature.device, "set_dummy", create=True) + dummy_feature.attribute_setter = "set_dummy" + await dummy_feature.set_value("dummy value") + mock_set_dummy.assert_called_with("dummy value") + + +async def test_feature_setter_read_only(dummy_feature): + """Verify that read-only feature raises an exception when trying to change it.""" + dummy_feature.attribute_setter = None + with pytest.raises(ValueError): + await dummy_feature.set_value("value for read only feature") diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index ba5ebc4f..efe6995b 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -67,7 +67,7 @@ async def test_invalid_connection(dev): async def test_initial_update_emeter(dev, mocker): """Test that the initial update performs second query if emeter is available.""" dev._last_update = None - dev._features = set() + dev._legacy_features = set() spy = mocker.spy(dev.protocol, "query") await dev.update() # Devices with small buffers may require 3 queries @@ -79,7 +79,7 @@ async def test_initial_update_emeter(dev, mocker): async def test_initial_update_no_emeter(dev, mocker): """Test that the initial update performs second query if emeter is available.""" dev._last_update = None - dev._features = set() + dev._legacy_features = set() spy = mocker.spy(dev.protocol, "query") await dev.update() # 2 calls are necessary as some devices crash on unexpected modules @@ -218,9 +218,9 @@ async def test_features(dev): """Make sure features is always accessible.""" sysinfo = dev._last_update["system"]["get_sysinfo"] if "feature" in sysinfo: - assert dev.features == set(sysinfo["feature"].split(":")) + assert dev._legacy_features == set(sysinfo["feature"].split(":")) else: - assert dev.features == set() + assert dev._legacy_features == set() @device_iot