Bladeren bron

Tests: improve general config tests.

Add some checks for basic entity and dps requirements that aways need
to be met.

This is part of eliminating the need for writing device specific tests.
Lately I have given up on this because the tests are taking too long
to run, and the influx of new devices is too fast for writing tests.

Ideally there should be a simple declarative test config for each
device.  As each device becomes fully covered by that, the device
specific test classes can be removed, leaving only enough explicit
tests to maintain test coverage of the source code.

Also removed usage of TestCase.subTest, since pytest has a bug that
hides most of the output when it is used, and doesn't output the
context anyway.  Instead specify a message in each assert that
contains the context.
Jason Rumney 3 jaren geleden
bovenliggende
commit
924fe030c2
8 gewijzigde bestanden met toevoegingen van 255 en 164 verwijderingen
  1. 5 3
      tests/helpers.py
  2. 16 11
      tests/mixins/binary_sensor.py
  3. 38 34
      tests/mixins/light.py
  4. 48 32
      tests/mixins/number.py
  5. 24 17
      tests/mixins/select.py
  6. 27 14
      tests/mixins/sensor.py
  7. 61 48
      tests/mixins/switch.py
  8. 36 5
      tests/test_device_config.py

+ 5 - 3
tests/helpers.py

@@ -5,7 +5,9 @@ from custom_components.tuya_local.device import TuyaLocalDevice
 
 
 @asynccontextmanager
-async def assert_device_properties_set(device: TuyaLocalDevice, properties: dict):
+async def assert_device_properties_set(
+    device: TuyaLocalDevice, properties: dict, msg=None
+):
     results = []
     provided = {}
 
@@ -28,8 +30,8 @@ async def assert_device_properties_set(device: TuyaLocalDevice, properties: dict
     finally:
         assert len(provided) == len(properties.keys())
         for p in properties:
-            assert p in provided
-            assert properties[p] == provided[p]
+            assert p in provided, msg
+            assert properties[p] == provided[p], msg
 
         for result in results:
             result.assert_awaited()

+ 16 - 11
tests/mixins/binary_sensor.py

@@ -57,20 +57,25 @@ class MultiBinarySensorTests:
 
     def test_multi_bsensor_device_class(self):
         for key, subject in self.multiBSensor.items():
-            with self.subTest(key):
-                self.assertEqual(subject.device_class, self.multiBSensorDevClass[key])
+            self.assertEqual(
+                subject.device_class,
+                self.multiBSensorDevClass[key],
+                f"device_class mismatch in {key}",
+            )
 
     def test_multi_bsensor_is_on(self):
         for key, subject in self.multiBSensor.items():
-            with self.subTest(key):
-                dps = self.multiBSensorDps[key]
-                onval, offval = self.multiBSensorTestData[key]
-                self.dps[dps] = onval
-                self.assertTrue(subject.is_on)
-                self.dps[dps] = offval
-                self.assertFalse(subject.is_on)
+            dps = self.multiBSensorDps[key]
+            onval, offval = self.multiBSensorTestData[key]
+            self.dps[dps] = onval
+            self.assertTrue(subject.is_on, f"{key} fails in ON state")
+            self.dps[dps] = offval
+            self.assertFalse(subject.is_on, f"{key} fails in OFF state")
 
     def test_multi_bsensor_extra_state_attributes(self):
         for key, subject in self.multiBSensor.items():
-            with self.subTest(key):
-                self.assertEqual(subject.extra_state_attributes, {})
+            self.assertEqual(
+                subject.extra_state_attributes,
+                {},
+                f"extra_state_attributes mismatch in {key}",
+            )

+ 38 - 34
tests/mixins/light.py

@@ -115,54 +115,58 @@ class MultiLightTests:
 
     def test_multi_light_is_on(self):
         for key, light in self.multiLight.items():
-            with self.subTest(key):
-                dp_id = self.multiLightDps[key]
-                self.dps[dp_id] = self.multiLightOn[key]
-                self.assertTrue(light.is_on)
-                self.dps[dp_id] = self.multiLightOff[key]
-                self.assertFalse(light.is_on)
+            dp_id = self.multiLightDps[key]
+            self.dps[dp_id] = self.multiLightOn[key]
+            self.assertTrue(light.is_on, f"{key} fails when ON")
+            self.dps[dp_id] = self.multiLightOff[key]
+            self.assertFalse(light.is_on, f"{key} fails when OFF")
 
     async def test_multi_light_turn_on(self):
         for key, light in self.multiLight.items():
-            with self.subTest(key):
-                self.dps[self.multiLightDps[key]] = self.multiLightOff[key]
-                async with assert_device_properties_set(
-                    light._device, {self.multiLightDps[key]: self.multiLightOn[key]}
-                ):
-                    await light.async_turn_on()
+            self.dps[self.multiLightDps[key]] = self.multiLightOff[key]
+            async with assert_device_properties_set(
+                light._device,
+                {self.multiLightDps[key]: self.multiLightOn[key]},
+                f"{key} failed to turn on",
+            ):
+                await light.async_turn_on()
 
     async def test_multi_light_turn_off(self):
         for key, light in self.multiLight.items():
-            with self.subTest(key):
-                async with assert_device_properties_set(
-                    light._device,
-                    {self.multiLightDps[key]: self.multiLightOff[key]},
-                ):
-                    await light.async_turn_off()
+            async with assert_device_properties_set(
+                light._device,
+                {self.multiLightDps[key]: self.multiLightOff[key]},
+                f"{key} failed to turn off",
+            ):
+                await light.async_turn_off()
 
     async def test_multi_light_toggle_turns_on_when_it_was_off(self):
         for key, light in self.multiLight.items():
-            with self.subTest(key):
-                self.dps[self.multiLightDps[key]] = self.multiLightOff[key]
-                async with assert_device_properties_set(
-                    light._device,
-                    {self.multiLightDps[key]: self.multiLightOn[key]},
-                ):
-                    await light.async_toggle()
+            self.dps[self.multiLightDps[key]] = self.multiLightOff[key]
+            async with assert_device_properties_set(
+                light._device,
+                {self.multiLightDps[key]: self.multiLightOn[key]},
+                f"{key} failed to toggle",
+            ):
+                await light.async_toggle()
 
     async def test_multi_light_toggle_turns_off_when_it_was_on(self):
         for key, light in self.multiLight.items():
-            with self.subTest(key):
-                self.dps[self.multiLightDps[key]] = self.multiLightOn[key]
-                async with assert_device_properties_set(
-                    light._device,
-                    {self.multiLightDps[key]: self.multiLightOff[key]},
-                ):
-                    await light.async_toggle()
+            self.dps[self.multiLightDps[key]] = self.multiLightOn[key]
+            async with assert_device_properties_set(
+                light._device,
+                {self.multiLightDps[key]: self.multiLightOff[key]},
+                f"{key} failed to toggle",
+            ):
+                await light.async_toggle()
 
     def test_multi_light_state_attributes(self):
-        for light in self.multiLight.values():
-            self.assertEqual(light.extra_state_attributes, {})
+        for key, light in self.multiLight.items():
+            self.assertEqual(
+                light.extra_state_attributes,
+                {},
+                f"{key} extra_state_attributes mismatch",
+            )
 
 
 class DimmableLightTests:

+ 48 - 32
tests/mixins/number.py

@@ -77,56 +77,72 @@ class MultiNumberTests:
 
     def test_multi_number_min_value(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                self.assertEqual(subject.native_min_value, self.multiNumberMin[key])
+            self.assertEqual(
+                subject.native_min_value,
+                self.multiNumberMin[key],
+                f"{key} min value mismatch",
+            )
 
     def test_multi_number_max_value(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                self.assertEqual(subject.native_max_value, self.multiNumberMax[key])
+            self.assertEqual(
+                subject.native_max_value,
+                self.multiNumberMax[key],
+                f"{key} max value mismatch",
+            )
 
     def test_multi_number_step(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                self.assertEqual(subject.native_step, self.multiNumberStep[key])
+            self.assertEqual(
+                subject.native_step,
+                self.multiNumberStep[key],
+                f"{key} step mismatch",
+            )
 
     def test_multi_number_mode(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                self.assertEqual(subject.mode, self.multiNumberMode[key])
+            self.assertEqual(
+                subject.mode,
+                self.multiNumberMode[key],
+                f"{key} mode mismatch",
+            )
 
     def test_multi_number_unit_of_measurement(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                self.assertEqual(
-                    subject.native_unit_of_measurement, self.multiNumberUnit[key]
-                )
+            self.assertEqual(
+                subject.native_unit_of_measurement,
+                self.multiNumberUnit[key],
+                f"{key} unit mismatch",
+            )
 
     def test_multi_number_value(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                val = min(
-                    max(self.multiNumberMin[key], self.multiNumberStep[key]),
-                    self.multiNumberMax[key],
-                )
-                dps_val = val * self.multiNumberScale[key]
-                self.dps[self.multiNumberDps[key]] = dps_val
-                self.assertEqual(subject.native_value, val)
+            val = min(
+                max(self.multiNumberMin[key], self.multiNumberStep[key]),
+                self.multiNumberMax[key],
+            )
+            dps_val = val * self.multiNumberScale[key]
+            self.dps[self.multiNumberDps[key]] = dps_val
+            self.assertEqual(subject.native_value, val, f"{key} value mismatch")
 
     async def test_multi_number_set_value(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                val = min(
-                    max(self.multiNumberMin[key], self.multiNumberStep[key]),
-                    self.multiNumberMax[key],
-                )
-                dps_val = val * self.multiNumberScale[key]
-                async with assert_device_properties_set(
-                    subject._device, {self.multiNumberDps[key]: dps_val}
-                ):
-                    await subject.async_set_native_value(val)
+            val = min(
+                max(self.multiNumberMin[key], self.multiNumberStep[key]),
+                self.multiNumberMax[key],
+            )
+            dps_val = val * self.multiNumberScale[key]
+            async with assert_device_properties_set(
+                subject._device,
+                {self.multiNumberDps[key]: dps_val},
+                f"{key} failed to set correct value",
+            ):
+                await subject.async_set_native_value(val)
 
     def test_multi_number_extra_state_attributes(self):
         for key, subject in self.multiNumber.items():
-            with self.subTest(key):
-                self.assertEqual(subject.extra_state_attributes, {})
+            self.assertEqual(
+                subject.extra_state_attributes,
+                {},
+                f"{key} extra_state_attributes mismatch",
+            )

+ 24 - 17
tests/mixins/select.py

@@ -46,29 +46,36 @@ class MultiSelectTests:
 
     def test_multiSelect_options(self):
         for key, subject in self.multiSelect.items():
-            with self.subTest(key):
-                self.assertCountEqual(
-                    subject.options,
-                    self.multiSelectOptions[key].values(),
-                )
+            self.assertCountEqual(
+                subject.options,
+                self.multiSelectOptions[key].values(),
+                f"{key} options mismatch",
+            )
 
     def test_multiSelect_current_option(self):
         for key, subject in self.multiSelect.items():
-            with self.subTest(key):
-                for dpsVal, val in self.multiSelectOptions[key].items():
-                    self.dps[self.multiSelectDps[key]] = dpsVal
-                    self.assertEqual(subject.current_option, val)
+            for dpsVal, val in self.multiSelectOptions[key].items():
+                self.dps[self.multiSelectDps[key]] = dpsVal
+                self.assertEqual(
+                    subject.current_option,
+                    val,
+                    f"{key} option mapping mismatch",
+                )
 
     async def test_multiSelect_select_option(self):
         for key, subject in self.multiSelect.items():
-            with self.subTest(key):
-                for dpsVal, val in self.multiSelectOptions[key].items():
-                    async with assert_device_properties_set(
-                        subject._device, {self.multiSelectDps[key]: dpsVal}
-                    ):
-                        await subject.async_select_option(val)
+            for dpsVal, val in self.multiSelectOptions[key].items():
+                async with assert_device_properties_set(
+                    subject._device,
+                    {self.multiSelectDps[key]: dpsVal},
+                    f"{key} failed to select expected option",
+                ):
+                    await subject.async_select_option(val)
 
     def test_multiSelect_extra_state_attributes(self):
         for key, subject in self.multiSelect.items():
-            with self.subTest(key):
-                self.assertEqual(subject.extra_state_attributes, {})
+            self.assertEqual(
+                subject.extra_state_attributes,
+                {},
+                f"{key} extra_state_attributes mismatch",
+            )

+ 27 - 14
tests/mixins/sensor.py

@@ -68,29 +68,42 @@ class MultiSensorTests:
 
     def test_multi_sensor_units(self):
         for key, subject in self.multiSensor.items():
-            with self.subTest(key):
-                self.assertEqual(
-                    subject.native_unit_of_measurement, self.multiSensorUnit[key]
-                )
+            self.assertEqual(
+                subject.native_unit_of_measurement,
+                self.multiSensorUnit[key],
+                f"{key} unit mismatch",
+            )
 
     def test_multi_sensor_device_class(self):
         for key, subject in self.multiSensor.items():
-            with self.subTest(key):
-                self.assertEqual(subject.device_class, self.multiSensorDevClass[key])
+            self.assertEqual(
+                subject.device_class,
+                self.multiSensorDevClass[key],
+                f"{key} device_class mismatch",
+            )
 
     def test_multi_sensor_state_class(self):
         for key, subject in self.multiSensor.items():
-            with self.subTest(key):
-                self.assertEqual(subject.state_class, self.multiSensorStateClass[key])
+            self.assertEqual(
+                subject.state_class,
+                self.multiSensorStateClass[key],
+                f"{key} state_class mismatch",
+            )
 
     def test_multi_sensor_value(self):
         for key, subject in self.multiSensor.items():
-            with self.subTest(key):
-                dpsval, val = self.multiSensorTestData[key]
-                self.dps[self.multiSensorDps[key]] = dpsval
-                self.assertEqual(subject.native_value, val)
+            dpsval, val = self.multiSensorTestData[key]
+            self.dps[self.multiSensorDps[key]] = dpsval
+            self.assertEqual(
+                subject.native_value,
+                val,
+                f"{key} value mapping not as expected",
+            )
 
     def test_multi_sensor_extra_state_attributes(self):
         for key, subject in self.multiSensor.items():
-            with self.subTest(key):
-                self.assertEqual(subject.extra_state_attributes, {})
+            self.assertEqual(
+                subject.extra_state_attributes,
+                {},
+                f"{key} extra_state_attributes mismatch",
+            )

+ 61 - 48
tests/mixins/switch.py

@@ -151,75 +151,88 @@ class MultiSwitchTests:
 
     def test_multi_switch_is_on(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                dp = self.multiSwitchDps[key]
-                self.dps[dp] = True
-                self.assertEqual(subject.is_on, True)
+            dp = self.multiSwitchDps[key]
+            self.dps[dp] = True
+            self.assertEqual(subject.is_on, True, f"{key} fails when ON")
 
-                self.dps[dp] = False
-                self.assertEqual(subject.is_on, False)
+            self.dps[dp] = False
+            self.assertEqual(subject.is_on, False, f"{key} fails when OFF")
 
     async def test_multi_switch_turn_on(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                async with assert_device_properties_set(
-                    subject._device, {self.multiSwitchDps[key]: True}
-                ):
-                    await subject.async_turn_on()
+            async with assert_device_properties_set(
+                subject._device,
+                {self.multiSwitchDps[key]: True},
+                f"{key} failed to turn on",
+            ):
+                await subject.async_turn_on()
 
     async def test_multi_switch_turn_off(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                async with assert_device_properties_set(
-                    subject._device, {self.multiSwitchDps[key]: False}
-                ):
-                    await subject.async_turn_off()
+            async with assert_device_properties_set(
+                subject._device,
+                {self.multiSwitchDps[key]: False},
+                f"{key} failed to turn off",
+            ):
+                await subject.async_turn_off()
 
     async def test_multi_switch_toggle_turns_on_when_it_was_off(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                dp = self.multiSwitchDps[key]
-                self.dps[dp] = False
+            dp = self.multiSwitchDps[key]
+            self.dps[dp] = False
 
-                async with assert_device_properties_set(subject._device, {dp: True}):
-                    await subject.async_toggle()
+            async with assert_device_properties_set(
+                subject._device, {dp: True}, f"{key} failed to toggle"
+            ):
+                await subject.async_toggle()
 
     async def test_multi_switch_toggle_turns_off_when_it_was_on(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                dp = self.multiSwitchDps[key]
-                self.dps[dp] = True
+            dp = self.multiSwitchDps[key]
+            self.dps[dp] = True
 
-                async with assert_device_properties_set(subject._device, {dp: False}):
-                    await subject.async_toggle()
+            async with assert_device_properties_set(
+                subject._device, {dp: False}, f"{key} failed to toggle"
+            ):
+                await subject.async_toggle()
 
     def test_multi_switch_device_class(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                self.assertEqual(subject.device_class, self.multiSwitchDevClass[key])
+            self.assertEqual(
+                subject.device_class,
+                self.multiSwitchDevClass[key],
+                f"{key} device_class mismatch",
+            )
 
     def test_multi_switch_current_power_w(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                dp = self.multiSwitchPowerDps.get(key)
-                if dp is None:
-                    self.assertIsNone(subject.current_power_w)
-                else:
-                    self.dps[dp] = 1234
-                    self.assertEqual(
-                        subject.current_power_w,
-                        1234 / self.multiSwitchPowerScale.get(key, 1),
-                    )
+            dp = self.multiSwitchPowerDps.get(key)
+            if dp is None:
+                self.assertIsNone(
+                    subject.current_power_w,
+                    f"{key} current_power_w unexpectedly exists",
+                )
+            else:
+                self.dps[dp] = 1234
+                self.assertEqual(
+                    subject.current_power_w,
+                    1234 / self.multiSwitchPowerScale.get(key, 1),
+                    f"{key} current_power_w not as expected",
+                )
 
     def test_multi_switch_state_attributes(self):
         for key, subject in self.multiSwitch.items():
-            with self.subTest(key):
-                dp = self.multiSwitchPowerDps.get(key)
-                if dp is None:
-                    self.assertEqual(subject.extra_state_attributes, {})
-                else:
-                    self.dps[dp] = 987 * self.multiSwitchPowerScale.get(key, 1)
-                    self.assertDictEqual(
-                        subject.extra_state_attributes,
-                        {"current_power_w": 987.0},
-                    )
+            dp = self.multiSwitchPowerDps.get(key)
+            if dp is None:
+                self.assertEqual(
+                    subject.extra_state_attributes,
+                    {},
+                    f"{key} has unexpected extra_state_attributes",
+                )
+            else:
+                self.dps[dp] = 987 * self.multiSwitchPowerScale.get(key, 1)
+                self.assertDictEqual(
+                    subject.extra_state_attributes,
+                    {"current_power_w": 987.0},
+                    f"{key} extra_state_attributes mismatch",
+                )

+ 36 - 5
tests/test_device_config.py

@@ -1,5 +1,5 @@
 """Test the config parser"""
-from unittest import IsolatedAsyncioTestCase
+from unittest import IsolatedAsyncioTestCase, TestCase
 from unittest.mock import MagicMock
 
 from custom_components.tuya_local.helpers.device_config import (
@@ -25,14 +25,43 @@ class TestDeviceConfig(IsolatedAsyncioTestCase):
             break
         self.assertTrue(found)
 
+    def check_entity(self, entity, cfg):
+        """
+        Check that the entity has a dps list and each dps has an id,
+        type and name.
+        """
+        self.assertIsNotNone(
+            entity._config.get("entity"), f"entity type missing in {cfg}"
+        )
+        e = entity.config_id
+        self.assertIsNotNone(
+            entity._config.get("dps"), f"dps missing from {e} in {cfg}"
+        )
+        for dp in entity.dps():
+            self.assertIsNotNone(
+                dp._config.get("id"), f"dp id missing from {e} in {cfg}"
+            )
+            self.assertIsNotNone(
+                dp._config.get("type"), f"dp type missing from {e} in {cfg}"
+            )
+            self.assertIsNotNone(
+                dp._config.get("name"), f"dp name missing from {e} in {cfg}"
+            )
+
     def test_config_files_parse(self):
         """
-        All configs should be parsable and have at least a name and primary entity.
+        All configs should be parsable and meet certain criteria
         """
         for cfg in available_configs():
             parsed = TuyaDeviceConfig(cfg)
-            self.assertIsNotNone(parsed.name)
-            self.assertIsNotNone(parsed.primary_entity)
+            self.assertIsNotNone(parsed._config.get("name"), f"name missing from {cfg}")
+            self.assertIsNotNone(
+                parsed._config.get("primary_entity"),
+                f"primary_entity missing from {cfg}",
+            )
+            self.check_entity(parsed.primary_entity, cfg)
+            for entity in parsed.secondary_entities():
+                self.check_entity(entity, cfg)
 
     # Most of the device_config functionality is exercised during testing of
     # the various supported devices.  These tests concentrate only on the gaps.
@@ -60,7 +89,9 @@ class TestDeviceConfig(IsolatedAsyncioTestCase):
             await voltage.async_set_value(mock_device, 230)
 
     def test_dps_values_returns_none_with_no_mapping(self):
-        """Test that a dps with no mapping returns None as its possible values"""
+        """
+        Test that a dps with no mapping returns None as its possible values
+        """
         mock_device = MagicMock()
         cfg = get_config("kogan_switch")
         voltage = cfg.primary_entity.find_dps("voltage_v")