-
-
Notifications
You must be signed in to change notification settings - Fork 568
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
Add a base to allow easier testing of devices #99
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
class DummyDevice: | ||
"""DummyDevice base class, you should inherit from this and call | ||
`super().__init__(args, kwargs)` to save the original state. | ||
|
||
This class provides helpers to test simple devices, for more complex | ||
ones you will want to extend the `return_values` accordingly. | ||
The basic idea is that the overloaded send() will read a wanted response | ||
based on the call from `return_values`. | ||
|
||
For changing values :func:`_set_state` will use :func:`pop()` to extract | ||
the first parameter and set the state accordingly. | ||
|
||
For a very simple device the following is enough, see :class:`TestPlug` | ||
for complete code. | ||
|
||
.. code-block:: | ||
self.return_values = { | ||
"get_prop": self._get_state, | ||
"power": lambda x: self._set_state("power", x) | ||
} | ||
|
||
""" | ||
def __init__(self, *args, **kwargs): | ||
self.start_state = self.state.copy() | ||
|
||
def send(self, command: str, parameters=None, retry_count=3): | ||
"""Overridden send() to return values from `self.return_values`.""" | ||
return self.return_values[command](parameters) | ||
|
||
def _reset_state(self): | ||
"""Revert back to the original state.""" | ||
self.state = self.start_state.copy() | ||
|
||
def _set_state(self, var, value): | ||
"""Set a state of a variable, | ||
the value is expected to be an array with length of 1.""" | ||
# print("setting %s = %s" % (var, value)) | ||
self.state[var] = value.pop(0) | ||
|
||
def _get_state(self, props): | ||
"""Return wanted properties""" | ||
return [self.state[x] for x in props if x in self.state] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from unittest import TestCase | ||
from miio import Plug | ||
from .dummies import DummyDevice | ||
import pytest | ||
|
||
|
||
class DummyPlug(DummyDevice, Plug): | ||
def __init__(self, *args, **kwargs): | ||
self.state = { | ||
'power': 'on', | ||
'temperature': 32, | ||
'current': 123, | ||
} | ||
self.return_values = { | ||
'get_prop': self._get_state, | ||
'set_power': lambda x: self._set_state("power", x), | ||
} | ||
super().__init__(args, kwargs) | ||
|
||
|
||
@pytest.fixture(scope="class") | ||
def plug(request): | ||
request.cls.device = DummyPlug() | ||
# TODO add ability to test on a real device | ||
|
||
|
||
@pytest.mark.usefixtures("plug") | ||
class TestPlug(TestCase): | ||
def is_on(self): | ||
return self.device.status().is_on | ||
|
||
def state(self): | ||
return self.device.status() | ||
|
||
def test_on(self): | ||
self.device.off() # ensure off | ||
|
||
start_state = self.is_on() | ||
assert start_state is False | ||
|
||
self.device.on() | ||
assert self.is_on() is True | ||
|
||
def test_off(self): | ||
self.device.on() # ensure on | ||
|
||
assert self.is_on() is True | ||
self.device.off() | ||
assert self.is_on() is False | ||
|
||
def test_status(self): | ||
self.device._reset_state() | ||
|
||
assert self.is_on() is True | ||
assert self.state().temperature == self.device.start_state["temperature"] | ||
assert self.state().load_power == self.device.start_state["current"] * 110 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. line too long (82 > 79 characters) |
||
|
||
def test_status_without_current(self): | ||
del self.device.state["current"] | ||
|
||
assert self.state().load_power is None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
from unittest import TestCase | ||
from miio import Yeelight | ||
from miio.yeelight import YeelightMode, YeelightStatus, YeelightException | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'miio.yeelight.YeelightStatus' imported but unused |
||
import pytest | ||
from .dummies import DummyDevice | ||
|
||
|
||
class DummyLight(DummyDevice, Yeelight): | ||
def __init__(self, *args, **kwargs): | ||
self.state = { | ||
'power': 'off', | ||
'bright': '100', | ||
'ct': '3584', | ||
'rgb': '16711680', | ||
'hue': '359', | ||
'sat': '100', | ||
'color_mode': '2', | ||
'name': 'test name', | ||
'lan_ctrl': '1', | ||
'save_state': '1' | ||
} | ||
|
||
self.return_values = { | ||
'get_prop': self._get_state, | ||
'set_power': lambda x: self._set_state("power", x), | ||
'set_bright': lambda x: self._set_state("bright", x), | ||
'set_ct_abx': lambda x: self._set_state("ct", x), | ||
'set_rgb': lambda x: self._set_state("rgb", x), | ||
'set_hsv': lambda x: self._set_state("hsv", x), | ||
'set_name': lambda x: self._set_state("name", x), | ||
'set_ps': lambda x: self.set_config(x), | ||
'toggle': self.toggle_power, | ||
'set_default': lambda x: 'ok' | ||
} | ||
|
||
super().__init__(*args, **kwargs) | ||
|
||
def set_config(self, x): | ||
key, value = x | ||
config_mapping = { | ||
'cfg_lan_ctrl': 'lan_ctrl', | ||
'cfg_save_state': 'save_state' | ||
} | ||
|
||
self._set_state(config_mapping[key], [value]) | ||
|
||
def toggle_power(self, _): | ||
if self.state["power"] == "on": | ||
self.state["power"] = "off" | ||
else: | ||
self.state["power"] = "on" | ||
|
||
|
||
@pytest.fixture(scope="class") | ||
def dummylight(request): | ||
request.cls.device = DummyLight() | ||
# TODO add ability to test on a real device | ||
|
||
|
||
@pytest.mark.usefixtures("dummylight") | ||
class TestYeelight(TestCase): | ||
def test_status(self): | ||
self.device._reset_state() | ||
status = self.device.status() # type: YeelightStatus | ||
assert status.name == self.device.start_state["name"] | ||
assert status.is_on is False | ||
assert status.brightness == 100 | ||
assert status.color_temp == 3584 | ||
assert status.color_mode == YeelightMode.ColorTemperature | ||
assert status.developer_mode is True | ||
assert status.save_state_on_change is True | ||
|
||
# following are tested in set mode tests | ||
# assert status.rgb == 16711680 | ||
# assert status.hsv == (359, 100, 100) | ||
|
||
def test_on(self): | ||
self.device.off() # make sure we are off | ||
assert self.device.status().is_on is False | ||
self.device.on() | ||
assert self.device.status().is_on is True | ||
|
||
def test_off(self): | ||
self.device.on() # make sure we are on | ||
assert self.device.status().is_on is True | ||
self.device.off() | ||
assert self.device.status().is_on is False | ||
|
||
def test_set_brightness(self): | ||
def brightness(): | ||
return self.device.status().brightness | ||
|
||
self.device.set_brightness(50) | ||
assert brightness() == 50 | ||
self.device.set_brightness(0) | ||
assert brightness() == 0 | ||
self.device.set_brightness(100) | ||
|
||
with pytest.raises(YeelightException): | ||
self.device.set_brightness(-100) | ||
|
||
with pytest.raises(YeelightException): | ||
self.device.set_brightness(200) | ||
|
||
def test_set_color_temp(self): | ||
def color_temp(): | ||
return self.device.status().color_temp | ||
|
||
self.device.set_color_temp(2000) | ||
assert color_temp() == 2000 | ||
self.device.set_color_temp(6500) | ||
assert color_temp() == 6500 | ||
|
||
with pytest.raises(YeelightException): | ||
self.device.set_color_temp(1000) | ||
|
||
with pytest.raises(YeelightException): | ||
self.device.set_color_temp(7000) | ||
|
||
@pytest.mark.skip("rgb is not properly implemented") | ||
def test_set_rgb(self): | ||
self.device._reset_state() | ||
assert self.device.status().rgb == 16711680 | ||
|
||
NEW_RGB = 16712222 | ||
self.set_rgb(NEW_RGB) | ||
assert self.device.status().rgb == NEW_RGB | ||
|
||
@pytest.mark.skip("hsv is not properly implemented") | ||
def test_set_hsv(self): | ||
self.reset_state() | ||
hue, sat, val = self.device.status().hsv | ||
assert hue == 359 | ||
assert sat == 100 | ||
assert val == 100 | ||
|
||
self.device.set_hsv() | ||
|
||
def test_set_developer_mode(self): | ||
def dev_mode(): | ||
return self.device.status().developer_mode | ||
|
||
orig_mode = dev_mode() | ||
self.device.set_developer_mode(not orig_mode) | ||
new_mode = dev_mode() | ||
assert new_mode is not orig_mode | ||
self.device.set_developer_mode(not new_mode) | ||
assert new_mode is not dev_mode() | ||
|
||
def test_set_save_state_on_change(self): | ||
def save_state(): | ||
return self.device.status().save_state_on_change | ||
|
||
orig_state = save_state() | ||
self.device.set_save_state_on_change(not orig_state) | ||
new_state = save_state() | ||
assert new_state is not orig_state | ||
self.device.set_save_state_on_change(not new_state) | ||
new_state = save_state() | ||
assert new_state is orig_state | ||
|
||
def test_set_name(self): | ||
def name(): | ||
return self.device.status().name | ||
|
||
assert name() == "test name" | ||
self.device.set_name("new test name") | ||
assert name() == "new test name" | ||
|
||
def test_toggle(self): | ||
def is_on(): | ||
return self.device.status().is_on | ||
|
||
orig_state = is_on() | ||
self.device.toggle() | ||
new_state = is_on() | ||
assert orig_state != new_state | ||
|
||
self.device.toggle() | ||
new_state = is_on() | ||
assert new_state == orig_state | ||
|
||
@pytest.mark.skip("cannot be tested easily") | ||
def test_set_default(self): | ||
self.fail() | ||
|
||
@pytest.mark.skip("set_scene is not implemented") | ||
def test_set_scene(self): | ||
self.fail() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
line too long (81 > 79 characters)