Skip to content

Commit

Permalink
Move mocked device and status into conftest (#1873)
Browse files Browse the repository at this point in the history
  • Loading branch information
rytilahti committed Dec 5, 2023
1 parent c2ddfaa commit dac61d4
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 102 deletions.
58 changes: 58 additions & 0 deletions miio/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest

from ..device import Device
from ..devicestatus import DeviceStatus, action, sensor, setting


@pytest.fixture()
def dummy_status():
"""Fixture for a status class with different sensors and settings."""

class Status(DeviceStatus):
@property
@sensor("sensor_without_unit")
def sensor_without_unit(self) -> int:
return 1

@property
@sensor("sensor_with_unit", unit="V")
def sensor_with_unit(self) -> int:
return 2

@property
@setting("setting_without_unit", setter_name="dummy")
def setting_without_unit(self):
return 3

@property
@setting("setting_with_unit", unit="V", setter_name="dummy")
def setting_with_unit(self):
return 4

@property
@sensor("none_sensor")
def sensor_returning_none(self):
return None

yield Status()


@pytest.fixture()
def dummy_device(mocker, dummy_status):
"""Returns a very basic device with patched out I/O and a dummy status."""

class DummyDevice(Device):
@action(id="test", name="test")
def test_action(self):
pass

d = DummyDevice("127.0.0.1", "68ffffffffffffffffffffffffffffff")
d._protocol._device_id = b"12345678"
mocker.patch("miio.Device.send")
mocker.patch("miio.Device.send_handshake")

patched_status = mocker.patch("miio.Device.status")
patched_status.__annotations__ = {}
patched_status.__annotations__["return"] = dummy_status

yield d
63 changes: 24 additions & 39 deletions miio/tests/test_descriptorcollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,20 @@
RangeDescriptor,
ValidSettingRange,
)
from miio.devicestatus import action, sensor, setting
from miio.devicestatus import sensor, setting


@pytest.fixture
def dev(mocker):
d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff")
mocker.patch("miio.Device.send")
mocker.patch("miio.Device.send_handshake")
yield d


def test_descriptors_from_device_object(mocker):
def test_descriptors_from_device_object(dummy_device):
"""Test descriptor collection from device class."""

class DummyDevice(Device):
@action(id="test", name="test")
def test_action(self):
pass

dev = DummyDevice("127.0.0.1", "68ffffffffffffffffffffffffffffff")
mocker.patch("miio.Device.send")
mocker.patch("miio.Device.send_handshake")

coll = DescriptorCollection(device=dev)
coll.descriptors_from_object(DummyDevice())
coll = DescriptorCollection(device=dummy_device)
coll.descriptors_from_object(dummy_device)
assert len(coll) == 1
assert isinstance(coll["test"], ActionDescriptor)


def test_descriptors_from_status_object(dev):
coll = DescriptorCollection(device=dev)
def test_descriptors_from_status_object(dummy_device):
coll = DescriptorCollection(device=dummy_device)

class TestStatus(DeviceStatus):
@sensor(id="test", name="test sensor")
Expand All @@ -67,21 +50,21 @@ def test_setting(self):
pytest.param(PropertyDescriptor, {"status_attribute": "foo"}),
],
)
def test_add_descriptor(dev: Device, cls, params):
def test_add_descriptor(dummy_device: Device, cls, params):
"""Test that adding a descriptor works."""
coll: DescriptorCollection = DescriptorCollection(device=dev)
coll: DescriptorCollection = DescriptorCollection(device=dummy_device)
coll.add_descriptor(cls(id="id", name="test name", **params))
assert len(coll) == 1
assert coll["id"] is not None


def test_handle_action_descriptor(mocker, dev):
coll = DescriptorCollection(device=dev)
def test_handle_action_descriptor(mocker, dummy_device):
coll = DescriptorCollection(device=dummy_device)
invalid_desc = ActionDescriptor(id="action", name="test name")
with pytest.raises(ValueError, match="Neither method or method_name was defined"):
coll.add_descriptor(invalid_desc)

mocker.patch.object(dev, "existing_method", create=True)
mocker.patch.object(dummy_device, "existing_method", create=True)

# Test method name binding
act_with_method_name = ActionDescriptor(
Expand All @@ -100,8 +83,8 @@ def test_handle_action_descriptor(mocker, dev):
coll.add_descriptor(act_with_method_name_missing)


def test_handle_writable_property_descriptor(mocker, dev):
coll = DescriptorCollection(device=dev)
def test_handle_writable_property_descriptor(mocker, dummy_device):
coll = DescriptorCollection(device=dummy_device)
data = {
"name": "",
"status_attribute": "",
Expand All @@ -111,7 +94,7 @@ def test_handle_writable_property_descriptor(mocker, dev):
with pytest.raises(ValueError, match="Neither setter or setter_name was defined"):
coll.add_descriptor(invalid)

mocker.patch.object(dev, "existing_method", create=True)
mocker.patch.object(dummy_device, "existing_method", create=True)

# Test name binding
setter_name_desc = PropertyDescriptor(
Expand All @@ -128,15 +111,15 @@ def test_handle_writable_property_descriptor(mocker, dev):
)


def test_handle_enum_constraints(dev, mocker):
coll = DescriptorCollection(device=dev)
def test_handle_enum_constraints(dummy_device, mocker):
coll = DescriptorCollection(device=dummy_device)

data = {
"name": "enum",
"status_attribute": "attr",
}

mocker.patch.object(dev, "choices_attr", create=True)
mocker.patch.object(dummy_device, "choices_attr", create=True)

# Check that error is raised if choices are missing
invalid = EnumDescriptor(id="missing", **data)
Expand All @@ -154,8 +137,8 @@ def test_handle_enum_constraints(dev, mocker):
assert coll["with_choices_attr"].choices is not None


def test_handle_range_constraints(dev, mocker):
coll = DescriptorCollection(device=dev)
def test_handle_range_constraints(dummy_device, mocker):
coll = DescriptorCollection(device=dummy_device)

data = {
"name": "name",
Expand All @@ -170,7 +153,9 @@ def test_handle_range_constraints(dev, mocker):
coll.add_descriptor(desc)
assert coll["regular"].max_value == 100

mocker.patch.object(dev, "range", create=True, new=ValidSettingRange(-1, 1000, 10))
mocker.patch.object(
dummy_device, "range", create=True, new=ValidSettingRange(-1, 1000, 10)
)
range_attr = RangeDescriptor(id="range_attribute", range_attribute="range", **data)
coll.add_descriptor(range_attr)

Expand All @@ -179,8 +164,8 @@ def test_handle_range_constraints(dev, mocker):
assert coll["range_attribute"].step == 10


def test_duplicate_identifiers(dev):
coll = DescriptorCollection(device=dev)
def test_duplicate_identifiers(dummy_device):
coll = DescriptorCollection(device=dummy_device)
for i in range(3):
coll.add_descriptor(
ActionDescriptor(id="action", name=f"action {i}", method=lambda _: _)
Expand Down
89 changes: 26 additions & 63 deletions miio/tests/test_devicestatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from miio import Device, DeviceStatus
from miio import DeviceStatus
from miio.descriptors import EnumDescriptor, RangeDescriptor, ValidSettingRange
from miio.devicestatus import sensor, setting

Expand Down Expand Up @@ -109,19 +109,19 @@ def unknown(self):
pass

status = DecoratedProps()
sensors = status.descriptors()
assert len(sensors) == 3
descs = status.descriptors()
assert len(descs) == 3

all_kwargs = sensors["all_kwargs"]
all_kwargs = descs["all_kwargs"]
assert all_kwargs.name == "Voltage"
assert all_kwargs.unit == "V"

assert sensors["only_name"].name == "Only name"
assert descs["only_name"].name == "Only name"

assert "unknown_kwarg" in sensors["unknown"].extras
assert "unknown_kwarg" in descs["unknown"].extras


def test_setting_decorator_number(mocker):
def test_setting_decorator_number(dummy_device, mocker):
"""Tests for setting decorator with numbers."""

class Settings(DeviceStatus):
Expand All @@ -137,24 +137,20 @@ class Settings(DeviceStatus):
def level(self) -> int:
return 1

mocker.patch("miio.Device.send")
d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff")
d._protocol._device_id = b"12345678"

# Patch status to return our class
status = mocker.patch.object(d, "status", return_value=Settings())
status = mocker.patch.object(dummy_device, "status", return_value=Settings())
status.__annotations__ = {}
status.__annotations__["return"] = Settings
# Patch to create a new setter as defined in the status class
setter = mocker.patch.object(d, "set_level", create=True)
setter = mocker.patch.object(dummy_device, "set_level", create=True)

settings = d.settings()
settings = dummy_device.settings()
assert len(settings) == 1

desc = settings["level"]
assert isinstance(desc, RangeDescriptor)

assert getattr(d.status(), desc.status_attribute) == 1
assert getattr(dummy_device.status(), desc.status_attribute) == 1

assert desc.name == "Level"
assert desc.min_value == 0
Expand All @@ -165,7 +161,7 @@ def level(self) -> int:
setter.assert_called_with(1)


def test_setting_decorator_number_range_attribute(mocker):
def test_setting_decorator_number_range_attribute(mocker, dummy_device):
"""Tests for setting decorator with range_attribute.
This makes sure the range_attribute overrides {min,max}_value and step.
Expand All @@ -186,26 +182,24 @@ class Settings(DeviceStatus):
def level(self) -> int:
return 1

mocker.patch("miio.Device.send")
d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff")
d._protocol._device_id = b"12345678"

# Patch status to return our class
status = mocker.patch.object(d, "status", return_value=Settings())
status = mocker.patch.object(dummy_device, "status", return_value=Settings())
status.__annotations__ = {}
status.__annotations__["return"] = Settings

mocker.patch.object(d, "valid_range", create=True, new=ValidSettingRange(1, 100, 2))
mocker.patch.object(
dummy_device, "valid_range", create=True, new=ValidSettingRange(1, 100, 2)
)
# Patch to create a new setter as defined in the status class
setter = mocker.patch.object(d, "set_level", create=True)
setter = mocker.patch.object(dummy_device, "set_level", create=True)

settings = d.settings()
settings = dummy_device.settings()
assert len(settings) == 1

desc = settings["level"]
assert isinstance(desc, RangeDescriptor)

assert getattr(d.status(), desc.status_attribute) == 1
assert getattr(dummy_device.status(), desc.status_attribute) == 1

assert desc.name == "Level"
assert desc.min_value == 1
Expand All @@ -216,7 +210,7 @@ def level(self) -> int:
setter.assert_called_with(50)


def test_setting_decorator_enum(mocker):
def test_setting_decorator_enum(dummy_device, mocker):
"""Tests for setting decorator with enums."""

class TestEnum(Enum):
Expand All @@ -235,23 +229,19 @@ class Settings(DeviceStatus):
def level(self) -> TestEnum:
return TestEnum.First

mocker.patch("miio.Device.send")
d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff")
d._protocol._device_id = b"12345678"

# Patch status to return our class
status = mocker.patch.object(d, "status", return_value=Settings())
status = mocker.patch.object(dummy_device, "status", return_value=Settings())
status.__annotations__ = {}
status.__annotations__["return"] = Settings
# Patch to create a new setter as defined in the status class
setter = mocker.patch.object(d, "set_level", create=True)
setter = mocker.patch.object(dummy_device, "set_level", create=True)

settings = d.settings()
settings = dummy_device.settings()
assert len(settings) == 1

desc = settings["level"]
assert isinstance(desc, EnumDescriptor)
assert getattr(d.status(), desc.status_attribute) == TestEnum.First
assert getattr(dummy_device.status(), desc.status_attribute) == TestEnum.First

assert desc.name == "Level"
assert len(desc.choices) == 2
Expand Down Expand Up @@ -301,42 +291,15 @@ def sub_sensor(self):
assert "SubStatus__sub_sensor" in dir(main)


def test_cli_output():
def test_cli_output(dummy_status):
"""Test the cli output string."""

class Status(DeviceStatus):
@property
@sensor("sensor_without_unit")
def sensor_without_unit(self) -> int:
return 1

@property
@sensor("sensor_with_unit", unit="V")
def sensor_with_unit(self) -> int:
return 2

@property
@setting("setting_without_unit", setter_name="dummy")
def setting_without_unit(self):
return 3

@property
@setting("setting_with_unit", unit="V", setter_name="dummy")
def setting_with_unit(self):
return 4

@property
@sensor("none_sensor")
def sensor_returning_none(self):
return None

status = Status()
expected_regex = [
"r-- sensor_without_unit (.+?): 1",
"r-- sensor_with_unit (.+?): 2 V",
r"rw- setting_without_unit (.+?): 3",
r"rw- setting_with_unit (.+?): 4 V",
]

for idx, line in enumerate(status.__cli_output__.splitlines()):
for idx, line in enumerate(dummy_status.__cli_output__.splitlines()):
assert re.match(expected_regex[idx], line) is not None

0 comments on commit dac61d4

Please sign in to comment.