diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 1dad1f3a1eb..5994184d815 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP CONF_MAX_ENTRIES = 'max_entries' +CONF_FIRE_EVENT = 'fire_event' CONF_MESSAGE = 'message' CONF_LEVEL = 'level' CONF_LOGGER = 'logger' DATA_SYSTEM_LOG = 'system_log' DEFAULT_MAX_ENTRIES = 50 +DEFAULT_FIRE_EVENT = False DEPENDENCIES = ['http'] DOMAIN = 'system_log' @@ -37,6 +39,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): cv.positive_int, + vol.Optional(CONF_FIRE_EVENT, default=DEFAULT_FIRE_EVENT): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -97,11 +100,12 @@ def _exception_as_string(exc_info): class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen): + def __init__(self, hass, maxlen, fire_event): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = deque(maxlen=maxlen) + self.fire_event = fire_event def _create_entry(self, record, call_stack): return { @@ -130,7 +134,8 @@ class LogErrorHandler(logging.Handler): entry = self._create_entry(record, stack) self.records.appendleft(entry) - self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + if self.fire_event: + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) @asyncio.coroutine @@ -140,7 +145,8 @@ def async_setup(hass, config): if conf is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) + handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], + conf[CONF_FIRE_EVENT]) logging.getLogger().addHandler(handler) hass.http.register_view(AllErrorsView(handler)) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index c440ef9c30c..59e99e5c1b5 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -1,33 +1,26 @@ """Test system log component.""" -import asyncio import logging from unittest.mock import MagicMock, patch -import pytest - from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log _LOGGER = logging.getLogger('test_logger') +BASIC_CONFIG = { + 'system_log': { + 'max_entries': 2, + } +} -@pytest.fixture(autouse=True) -@asyncio.coroutine -def setup_test_case(hass, aiohttp_client): - """Setup system_log component before test case.""" - config = {'system_log': {'max_entries': 2}} - yield from async_setup_component(hass, system_log.DOMAIN, config) - - -@asyncio.coroutine -def get_error_log(hass, aiohttp_client, expected_count): +async def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.get('/api/error/all') + client = await aiohttp_client(hass.http.app) + resp = await client.get('/api/error/all') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert len(data) == expected_count return data @@ -52,43 +45,43 @@ def get_frame(name): return (name, None, None, None) -@asyncio.coroutine -def test_normal_logs(hass, aiohttp_client): +async def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_exception(hass, aiohttp_client): +async def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') -@asyncio.coroutine -def test_warning(hass, aiohttp_client): +async def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') -@asyncio.coroutine -def test_error(hass, aiohttp_client): +async def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_error_posted_as_event(hass, aiohttp_client): - """Test that error are posted as events.""" +async def test_config_not_fire_event(hass): + """Test that errors are not posted as events with default config.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) events = [] @callback @@ -99,77 +92,100 @@ def test_error_posted_as_event(hass, aiohttp_client): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) _LOGGER.error('error message') - yield from hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_error_posted_as_event(hass): + """Test that error are posted as events.""" + await async_setup_component(hass, system_log.DOMAIN, { + 'system_log': { + 'max_entries': 2, + 'fire_event': True, + } + }) + events = [] + + @callback + def event_listener(event): + """Listen to events of type system_log_event.""" + events.append(event) + + hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) + + _LOGGER.error('error message') + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_critical(hass, aiohttp_client): +async def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') -@asyncio.coroutine -def test_remove_older_logs(hass, aiohttp_client): +async def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, aiohttp_client, 2) + log = await get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') -@asyncio.coroutine -def test_clear_logs(hass, aiohttp_client): +async def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_CLEAR, {})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_write_log(hass): +async def test_write_log(hass): """Test that error propagates to logger.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger) as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'homeassistant.components.system_log.external') assert logger.method_calls[0] == ('error', ('test_message',)) -@asyncio.coroutine -def test_write_choose_logger(hass): +async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('logging.getLogger') as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'logger': 'myLogger'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'myLogger') -@asyncio.coroutine -def test_write_choose_level(hass): +async def test_write_choose_level(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger): hass.async_add_job( @@ -177,17 +193,17 @@ def test_write_choose_level(hass): system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'level': 'debug'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert logger.method_calls[0] == ('debug', ('test_message',)) -@asyncio.coroutine -def test_unknown_path(hass, aiohttp_client): +async def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -206,31 +222,31 @@ def log_error_from_test_path(path): _LOGGER.error('error message') -@asyncio.coroutine -def test_homeassistant_path(hass, aiohttp_client): +async def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' -@asyncio.coroutine -def test_config_path(hass, aiohttp_client): +async def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' -@asyncio.coroutine -def test_netdisco_path(hass, aiohttp_client): +async def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py'