diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..48a4e188 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 017be7ad..4290b283 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,16 @@ repos: rev: 19.10b0 hooks: - id: black - language_version: python3.8 +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 + files: apps/controllerx +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.770 + hooks: + - id: mypy + files: apps/controllerx - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: @@ -12,4 +21,12 @@ repos: args: - --autofix - --indent - - "4" \ No newline at end of file + - "4" +- repo: local + hooks: + - id: pytest + name: pytest + entry: pytest + language: system + pass_filenames: false + always_run: true \ No newline at end of file diff --git a/apps/controllerx/core/controller.py b/apps/controllerx/core/controller.py index eaf4475d..526cb141 100644 --- a/apps/controllerx/core/controller.py +++ b/apps/controllerx/core/controller.py @@ -231,7 +231,7 @@ def get_type_actions_mapping(self) -> TypeActionsMapping: class TypeController(Controller, abc.ABC): @abc.abstractmethod def get_domain(self) -> str: - ... + raise NotImplementedError async def check_domain(self, entity: str) -> None: domain = self.get_domain() @@ -277,13 +277,14 @@ async def before_action(self, action: str, *args, **kwargs) -> bool: to_return = not (action == "hold" and self.on_hold) return await super().before_action(action, *args, **kwargs) and to_return + @abc.abstractmethod async def hold_loop(self, *args) -> bool: """ This function is called by the ReleaseHoldController depending on the settings. It stops calling the function once release action is called or when this function returns True. """ - return True + raise NotImplementedError def default_delay(self) -> int: """ diff --git a/apps/controllerx/core/custom_controller.py b/apps/controllerx/core/custom_controller.py index 9a1a95bb..df2e085f 100644 --- a/apps/controllerx/core/custom_controller.py +++ b/apps/controllerx/core/custom_controller.py @@ -21,7 +21,7 @@ def parse_action(self, action: str) -> TypeAction: It should eiter return a value parsed by 'get_type_actions_mapping' or a tuple with (function, arg1, arg2, ...). """ - ... + raise NotImplementedError def get_z2m_actions_mapping(self) -> TypeActionsMapping: return self.get_custom_mapping() diff --git a/apps/controllerx/core/integration/__init__.py b/apps/controllerx/core/integration/__init__.py index 32a124aa..d1ba8fd8 100644 --- a/apps/controllerx/core/integration/__init__.py +++ b/apps/controllerx/core/integration/__init__.py @@ -15,15 +15,15 @@ def __init__(self, controller, kwargs: Dict[str, Any]): @abc.abstractmethod def get_name(self) -> str: - ... + raise NotImplementedError @abc.abstractmethod def get_actions_mapping(self) -> Optional[TypeActionsMapping]: - ... + raise NotImplementedError @abc.abstractmethod def listen_changes(self, controller_id: str) -> None: - ... + raise NotImplementedError def _import_modules(file_dir: str, package: str) -> None: diff --git a/apps/controllerx/core/stepper/__init__.py b/apps/controllerx/core/stepper/__init__.py index 385907f1..c1d20a92 100644 --- a/apps/controllerx/core/stepper/__init__.py +++ b/apps/controllerx/core/stepper/__init__.py @@ -36,4 +36,4 @@ def step(self, value: float, direction: str) -> Tuple[Union[int, float], bool]: that needs to take and returns the new value and True if the step exceeds the boundaries. """ - ... + raise NotImplementedError diff --git a/tests/core/controller_test.py b/tests/core/controller_test.py index b33044d6..1bda7d87 100644 --- a/tests/core/controller_test.py +++ b/tests/core/controller_test.py @@ -6,10 +6,10 @@ from core import integration as integration_module from core.controller import action from tests.test_utils import ( - hass_mock, - fake_controller, IntegrationMock, fake_async_function, + fake_controller, + hass_mock, ) @@ -160,6 +160,7 @@ async def test_initialize( ("sensor1, sensor2", ["sensor1", "sensor2"]), ("sensor1,sensor2", ["sensor1", "sensor2"]), (["sensor1", "sensor2"], ["sensor1", "sensor2"]), + (0.0, []), ], ) def test_get_list(sut, monkeypatch, test_input, expected): @@ -194,6 +195,7 @@ def test_get_option(sut, option, options, expect_an_error): False, ), ({"test": "no name"}, "z2m", {}, True), + (0.0, None, {}, True), ], ) def test_get_integration( @@ -347,3 +349,19 @@ def test_get_action(sut, test_input, expected, error_expected): else: output = sut.get_action(test_input) assert output == expected + + +@pytest.mark.parametrize( + "service, attributes", + [("test_service", {"attr1": 0.0, "attr2": "test"}), ("test_service", {}),], +) +@pytest.mark.asyncio +async def test_call_service(sut, mocker, service, attributes): + + call_service_stub = mocker.patch.object(hass.Hass, "call_service") + + # SUT + await sut.call_service(service, **attributes) + + # Checker + call_service_stub.assert_called_once_with(service, **attributes) diff --git a/tests/core/type/light_controller_test.py b/tests/core/type/light_controller_test.py index d61aa47d..f3750729 100644 --- a/tests/core/type/light_controller_test.py +++ b/tests/core/type/light_controller_test.py @@ -22,23 +22,30 @@ def sut(hass_mock, monkeypatch): @pytest.mark.parametrize( - "light_input, light_output", + "light_input, light_output, error_expected", [ - ("light.kitchen", {"name": "light.kitchen", "color_mode": "auto"}), + ("light.kitchen", {"name": "light.kitchen", "color_mode": "auto"}, False), ( {"name": "light.kitchen", "color_mode": "auto"}, {"name": "light.kitchen", "color_mode": "auto"}, + False, + ), + ( + {"name": "light.kitchen"}, + {"name": "light.kitchen", "color_mode": "auto"}, + False, ), - ({"name": "light.kitchen"}, {"name": "light.kitchen", "color_mode": "auto"}), ( {"name": "light.kitchen", "color_mode": "color_temp"}, {"name": "light.kitchen", "color_mode": "color_temp"}, + False, ), + (0.0, None, True), ], ) @pytest.mark.asyncio async def test_initialize_and_get_light( - sut, monkeypatch, mocker, light_input, light_output + sut, monkeypatch, mocker, light_input, light_output, error_expected ): super_initialize_stub = mocker.stub() @@ -48,10 +55,17 @@ async def fake_super_initialize(self): monkeypatch.setattr(ReleaseHoldController, "initialize", fake_super_initialize) sut.args["light"] = light_input - await sut.initialize() - super_initialize_stub.assert_called_once() - assert sut.light == light_output + # SUT + if error_expected: + with pytest.raises(ValueError) as e: + await sut.initialize() + else: + await sut.initialize() + + # Checks + super_initialize_stub.assert_called_once() + assert sut.light == light_output @pytest.mark.parametrize( @@ -97,20 +111,34 @@ def test_get_attribute( @pytest.mark.parametrize( - "attribute_input, expected_output", - [("xy_color", 0), ("brightness", 3.0), ("color_temp", 1),], + "attribute_input, expected_output, error_expected", + [ + ("xy_color", 0, False), + ("brightness", 3.0, False), + ("color_temp", 1, False), + ("xy_color", 0, False), + ("brightness", None, True), + ("color_temp", None, True), + ], ) @pytest.mark.asyncio -async def test_get_value_attribute(sut, monkeypatch, attribute_input, expected_output): +async def test_get_value_attribute( + sut, monkeypatch, attribute_input, expected_output, error_expected +): async def fake_get_entity_state(entity, attribute): return expected_output monkeypatch.setattr(sut, "get_entity_state", fake_get_entity_state) # SUT - output = await sut.get_value_attribute(attribute_input) + if error_expected: + with pytest.raises(ValueError) as e: + await sut.get_value_attribute(attribute_input) + else: + output = await sut.get_value_attribute(attribute_input) - assert output == expected_output + # Checks + assert output == expected_output @pytest.mark.parametrize( @@ -459,15 +487,24 @@ def fake_get_attribute(*args, **kwargs): super_hold_patch.assert_called_with(attribute_input, expected_direction) +@pytest.mark.parametrize( + "value_attribute", [10, None], +) @pytest.mark.asyncio -async def test_hold_loop(sut, mocker): +async def test_hold_loop(sut, mocker, value_attribute): attribute = "test_attribute" direction = Stepper.UP - sut.value_attribute = 10 + sut.value_attribute = value_attribute change_light_state_patch = mocker.patch.object(sut, "change_light_state") stepper = MinMaxStepper(1, 10, 10) sut.automatic_steppers = {attribute: stepper} - await sut.hold_loop(attribute, direction) - change_light_state_patch.assert_called_once_with( - sut.value_attribute, attribute, direction, stepper, "hold" - ) + + # SUT + exceeded = await sut.hold_loop(attribute, direction) + + if value_attribute is None: + assert exceeded == True + else: + change_light_state_patch.assert_called_once_with( + sut.value_attribute, attribute, direction, stepper, "hold" + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 38a02f35..7bf32607 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -30,7 +30,7 @@ def fake_fn(*args, **kwargs): monkeypatch.setattr(hass.Hass, "listen_event", fake_fn) monkeypatch.setattr(hass.Hass, "listen_state", fake_fn) monkeypatch.setattr(hass.Hass, "log", fake_fn) - monkeypatch.setattr(hass.Hass, "call_service", fake_fn) + monkeypatch.setattr(hass.Hass, "call_service", fake_async_function()) @pytest.fixture