Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move mocked device and status into conftest #1873

Merged
merged 1 commit into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading