hass-core/tests/components/demo/test_notify.py
Phil Bruckner b2d7bc40dc
Add support for simultaneous runs of Script helper (#31937)
* Add tests for legacy Script helper behavior

* Add Script helper if_running and run_mode options

- if_running controls what happens if Script run while previous run
  has not completed. Can be:
  - error: Raise an exception
  - ignore: Return without doing anything (previous run continues as-is)
  - parallel: Start run in new task
  - restart: Stop previous run before starting new run
- run_mode controls when call to async_run will return. Can be:
  - background: Returns immediately
  - legacy: Implements previous behavior, which is to return when done,
            or when suspended by delay or wait_template
  - blocking: Returns when run has completed
- If neither is specified, default is run_mode=legacy (and if_running
  is not used.) Otherwise, defaults are if_running=parallel and
  run_mode=background. If run_mode is set to legacy then if_running must
  be None.
- Caller may supply a logger which will be used throughout instead of
  default module logger.
- Move Script running state into new helper classes, comprised of an
  abstract base class and two concrete clases, one for legacy behavior
  and one for new behavior.
- Remove some non-async methods, as well as call_from_config which has
  only been used in tests.
- Adjust tests accordingly.

* Change per review

- Change run_mode default from background to blocking.
- Make sure change listener is called, even when there's an unexpected
  exception.
- Make _ScriptRun.async_stop more graceful by using an asyncio.Event for
  signaling instead of simply cancelling Task.
- Subclass _ScriptRun for background & blocking behavior.

Also:

- Fix timeouts in _ScriptRun by converting timedeltas to float seconds.
- General cleanup.

* Change per review 2

- Don't propagate exceptions if call from user has already returned
  (i.e., for background runs or legacy runs that have suspended.)
- Allow user to specify if exceptions should be logged. They will still
  be logged regardless if exception is not propagated.
- Rename _start_script_delay and _start_wait_template_delay for
  clarity.
- Remove return value from Script.async_run.
- Fix missing await.
- Change call to self.is_running in Script.async_run to direct test of
  self._runs.

* Change per review 3 and add tests

- Remove Script.set_logger().
- Enhance existing tests to check all run modes.
- Add tests for new features.
- Fix a few minor bugs found by tests.
2020-02-24 14:56:00 -08:00

195 lines
6.9 KiB
Python

"""The tests for the notify demo platform."""
import unittest
from unittest.mock import patch
import pytest
import voluptuous as vol
import homeassistant.components.demo.notify as demo
import homeassistant.components.notify as notify
from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.setup import setup_component
from tests.common import assert_setup_component, get_test_home_assistant
from tests.components.notify import common
CONFIG = {notify.DOMAIN: {"platform": "demo"}}
class TestNotifyDemo(unittest.TestCase):
"""Test the demo notify."""
def setUp(self): # pylint: disable=invalid-name
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.events = []
self.calls = []
@callback
def record_event(event):
"""Record event to send notification."""
self.events.append(event)
self.hass.bus.listen(demo.EVENT_NOTIFY, record_event)
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def _setup_notify(self):
with assert_setup_component(1, notify.DOMAIN) as config:
assert setup_component(self.hass, notify.DOMAIN, CONFIG)
assert config[notify.DOMAIN]
self.hass.block_till_done()
def test_setup(self):
"""Test setup."""
self._setup_notify()
@patch("homeassistant.components.demo.notify.get_service", autospec=True)
def test_no_notify_service(self, mock_demo_get_service):
"""Test missing platform notify service instance."""
mock_demo_get_service.return_value = None
with self.assertLogs(
"homeassistant.components.notify", level="ERROR"
) as log_handle:
self._setup_notify()
self.hass.block_till_done()
assert mock_demo_get_service.called
assert log_handle.output == [
"ERROR:homeassistant.components.notify:"
"Failed to initialize notification service demo"
]
@patch("homeassistant.components.demo.notify.get_service", autospec=True)
def test_discover_notify(self, mock_demo_get_service):
"""Test discovery of notify demo platform."""
assert notify.DOMAIN not in self.hass.config.components
discovery.load_platform(
self.hass, "notify", "demo", {"test_key": "test_val"}, {"notify": {}}
)
self.hass.block_till_done()
assert notify.DOMAIN in self.hass.config.components
assert mock_demo_get_service.called
assert mock_demo_get_service.mock_calls[0][1] == (
self.hass,
{},
{"test_key": "test_val"},
)
@callback
def record_calls(self, *args):
"""Record calls."""
self.calls.append(args)
def test_sending_none_message(self):
"""Test send with None as message."""
self._setup_notify()
with pytest.raises(vol.Invalid):
common.send_message(self.hass, None)
self.hass.block_till_done()
assert len(self.events) == 0
def test_sending_templated_message(self):
"""Send a templated message."""
self._setup_notify()
self.hass.states.set("sensor.temperature", 10)
common.send_message(
self.hass,
"{{ states.sensor.temperature.state }}",
"{{ states.sensor.temperature.name }}",
)
self.hass.block_till_done()
last_event = self.events[-1]
assert last_event.data[notify.ATTR_TITLE] == "temperature"
assert last_event.data[notify.ATTR_MESSAGE] == "10"
def test_method_forwards_correct_data(self):
"""Test that all data from the service gets forwarded to service."""
self._setup_notify()
common.send_message(self.hass, "my message", "my title", {"hello": "world"})
self.hass.block_till_done()
assert len(self.events) == 1
data = self.events[0].data
assert {
"message": "my message",
"title": "my title",
"data": {"hello": "world"},
} == data
def test_calling_notify_from_script_loaded_from_yaml_without_title(self):
"""Test if we can call a notify from a script."""
self._setup_notify()
step = {
"service": "notify.notify",
"data": {
"data": {
"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}
}
},
"data_template": {"message": "Test 123 {{ 2 + 2 }}\n"},
}
setup_component(self.hass, "script", {"script": {"test": {"sequence": step}}})
self.hass.services.call("script", "test")
self.hass.block_till_done()
assert len(self.events) == 1
assert {
"message": "Test 123 4",
"data": {
"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}
},
} == self.events[0].data
def test_calling_notify_from_script_loaded_from_yaml_with_title(self):
"""Test if we can call a notify from a script."""
self._setup_notify()
step = {
"service": "notify.notify",
"data": {
"data": {
"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}
}
},
"data_template": {"message": "Test 123 {{ 2 + 2 }}\n", "title": "Test"},
}
setup_component(self.hass, "script", {"script": {"test": {"sequence": step}}})
self.hass.services.call("script", "test")
self.hass.block_till_done()
assert len(self.events) == 1
assert {
"message": "Test 123 4",
"title": "Test",
"data": {
"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}
},
} == self.events[0].data
def test_targets_are_services(self):
"""Test that all targets are exposed as individual services."""
self._setup_notify()
assert self.hass.services.has_service("notify", "demo") is not None
service = "demo_test_target_name"
assert self.hass.services.has_service("notify", service) is not None
def test_messages_to_targets_route(self):
"""Test message routing to specific target services."""
self._setup_notify()
self.hass.bus.listen_once("notify", self.record_calls)
self.hass.services.call(
"notify",
"demo_test_target_name",
{"message": "my message", "title": "my title", "data": {"hello": "world"}},
)
self.hass.block_till_done()
data = self.calls[0][0].data
assert {
"message": "my message",
"target": ["test target id"],
"title": "my title",
"data": {"hello": "world"},
} == data