diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 16d5c750172..7daaf937975 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -116,6 +116,6 @@ class AsyncHandler(object): return self.handler.get_name() @name.setter - def set_name(self, name): + def name(self, name): """Wrap property get_name to handler.""" self.handler.name = name diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 5027e36a7f2..15dda24a529 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -51,6 +51,85 @@ def test_caching_data(hass): assert DATA_RESTORE_CACHE not in hass.data +@asyncio.coroutine +def test_hass_running(hass): + """Test that cache cannot be accessed while hass is running.""" + mock_component(hass, 'recorder') + + states = [ + State('input_boolean.b0', 'on'), + State('input_boolean.b1', 'on'), + State('input_boolean.b2', 'on'), + ] + + with patch('homeassistant.helpers.restore_state.last_recorder_run', + return_value=MagicMock(end=dt_util.utcnow())), \ + patch('homeassistant.helpers.restore_state.get_states', + return_value=states), \ + patch('homeassistant.helpers.restore_state.wait_connection_ready', + return_value=mock_coro(True)): + state = yield from async_get_last_state(hass, 'input_boolean.b1') + assert state is None + + +@asyncio.coroutine +def test_not_connected(hass): + """Test that cache cannot be accessed if db connection times out.""" + mock_component(hass, 'recorder') + hass.state = CoreState.starting + + states = [State('input_boolean.b1', 'on')] + + with patch('homeassistant.helpers.restore_state.last_recorder_run', + return_value=MagicMock(end=dt_util.utcnow())), \ + patch('homeassistant.helpers.restore_state.get_states', + return_value=states), \ + patch('homeassistant.helpers.restore_state.wait_connection_ready', + return_value=mock_coro(False)): + state = yield from async_get_last_state(hass, 'input_boolean.b1') + assert state is None + + +@asyncio.coroutine +def test_no_last_run_found(hass): + """Test that cache cannot be accessed if no last run found.""" + mock_component(hass, 'recorder') + hass.state = CoreState.starting + + states = [State('input_boolean.b1', 'on')] + + with patch('homeassistant.helpers.restore_state.last_recorder_run', + return_value=None), \ + patch('homeassistant.helpers.restore_state.get_states', + return_value=states), \ + patch('homeassistant.helpers.restore_state.wait_connection_ready', + return_value=mock_coro(True)): + state = yield from async_get_last_state(hass, 'input_boolean.b1') + assert state is None + + +@asyncio.coroutine +def test_cache_timeout(hass): + """Test that cache timeout returns none.""" + mock_component(hass, 'recorder') + hass.state = CoreState.starting + + states = [State('input_boolean.b1', 'on')] + + @asyncio.coroutine + def timeout_coro(): + raise asyncio.TimeoutError() + + with patch('homeassistant.helpers.restore_state.last_recorder_run', + return_value=MagicMock(end=dt_util.utcnow())), \ + patch('homeassistant.helpers.restore_state.get_states', + return_value=states), \ + patch('homeassistant.helpers.restore_state.wait_connection_ready', + return_value=timeout_coro()): + state = yield from async_get_last_state(hass, 'input_boolean.b1') + assert state is None + + def _add_data_in_last_run(hass, entities): """Add test data in the last recorder_run.""" # pylint: disable=protected-access diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 1d6e669e1d6..b7a18d00fae 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -8,52 +8,74 @@ import pytest from homeassistant.util import async as hasync -@patch('asyncio.coroutines.iscoroutine', return_value=True) +@patch('asyncio.coroutines.iscoroutine') @patch('concurrent.futures.Future') @patch('threading.get_ident') -def test_run_coroutine_threadsafe_from_inside_event_loop(mock_ident, _, __): +def test_run_coroutine_threadsafe_from_inside_event_loop( + mock_ident, _, mock_iscoroutine): """Testing calling run_coroutine_threadsafe from inside an event loop.""" coro = MagicMock() loop = MagicMock() loop._thread_ident = None mock_ident.return_value = 5 + mock_iscoroutine.return_value = True hasync.run_coroutine_threadsafe(coro, loop) assert len(loop.call_soon_threadsafe.mock_calls) == 1 loop._thread_ident = 5 mock_ident.return_value = 5 + mock_iscoroutine.return_value = True with pytest.raises(RuntimeError): hasync.run_coroutine_threadsafe(coro, loop) assert len(loop.call_soon_threadsafe.mock_calls) == 1 loop._thread_ident = 1 mock_ident.return_value = 5 + mock_iscoroutine.return_value = False + with pytest.raises(TypeError): + hasync.run_coroutine_threadsafe(coro, loop) + assert len(loop.call_soon_threadsafe.mock_calls) == 1 + + loop._thread_ident = 1 + mock_ident.return_value = 5 + mock_iscoroutine.return_value = True hasync.run_coroutine_threadsafe(coro, loop) assert len(loop.call_soon_threadsafe.mock_calls) == 2 -@patch('asyncio.coroutines.iscoroutine', return_value=True) +@patch('asyncio.coroutines.iscoroutine') @patch('concurrent.futures.Future') @patch('threading.get_ident') -def test_fire_coroutine_threadsafe_from_inside_event_loop(mock_ident, _, __): +def test_fire_coroutine_threadsafe_from_inside_event_loop( + mock_ident, _, mock_iscoroutine): """Testing calling fire_coroutine_threadsafe from inside an event loop.""" coro = MagicMock() loop = MagicMock() loop._thread_ident = None mock_ident.return_value = 5 + mock_iscoroutine.return_value = True hasync.fire_coroutine_threadsafe(coro, loop) assert len(loop.call_soon_threadsafe.mock_calls) == 1 loop._thread_ident = 5 mock_ident.return_value = 5 + mock_iscoroutine.return_value = True with pytest.raises(RuntimeError): hasync.fire_coroutine_threadsafe(coro, loop) assert len(loop.call_soon_threadsafe.mock_calls) == 1 loop._thread_ident = 1 mock_ident.return_value = 5 + mock_iscoroutine.return_value = False + with pytest.raises(TypeError): + hasync.fire_coroutine_threadsafe(coro, loop) + assert len(loop.call_soon_threadsafe.mock_calls) == 1 + + loop._thread_ident = 1 + mock_ident.return_value = 5 + mock_iscoroutine.return_value = True hasync.fire_coroutine_threadsafe(coro, loop) assert len(loop.call_soon_threadsafe.mock_calls) == 2 @@ -82,7 +104,7 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): assert len(loop.call_soon_threadsafe.mock_calls) == 2 -class RunCoroutineThreadsafeTests(test_utils.TestCase): +class RunThreadsafeTests(test_utils.TestCase): """Test case for asyncio.run_coroutine_threadsafe.""" def setUp(self): @@ -91,26 +113,41 @@ class RunCoroutineThreadsafeTests(test_utils.TestCase): self.loop = asyncio.new_event_loop() self.set_event_loop(self.loop) # Will cleanup properly - @asyncio.coroutine - def add(self, a, b, fail=False, cancel=False): - """Wait 0.05 second and return a + b.""" - yield from asyncio.sleep(0.05, loop=self.loop) + def add_callback(self, a, b, fail, invalid): + """Return a + b.""" if fail: raise RuntimeError("Fail!") + if invalid: + raise ValueError("Invalid!") + return a + b + + @asyncio.coroutine + def add_coroutine(self, a, b, fail, invalid, cancel): + """Wait 0.05 second and return a + b.""" + yield from asyncio.sleep(0.05, loop=self.loop) if cancel: asyncio.tasks.Task.current_task(self.loop).cancel() yield - return a + b + return self.add_callback(a, b, fail, invalid) - def target(self, fail=False, cancel=False, timeout=None, - advance_coro=False): + def target_callback(self, fail=False, invalid=False): + """Run add callback in the event loop.""" + future = hasync.run_callback_threadsafe( + self.loop, self.add_callback, 1, 2, fail, invalid) + try: + return future.result() + finally: + future.done() or future.cancel() + + def target_coroutine(self, fail=False, invalid=False, cancel=False, + timeout=None, advance_coro=False): """Run add coroutine in the event loop.""" - coro = self.add(1, 2, fail=fail, cancel=cancel) + coro = self.add_coroutine(1, 2, fail, invalid, cancel) future = hasync.run_coroutine_threadsafe(coro, self.loop) if advance_coro: # this is for test_run_coroutine_threadsafe_task_factory_exception; # otherwise it spills errors and breaks **other** unittests, since - # 'target' is interacting with threads. + # 'target_coroutine' is interacting with threads. # With this call, `coro` will be advanced, so that # CoroWrapper.__del__ won't do anything when asyncio tests run @@ -123,20 +160,28 @@ class RunCoroutineThreadsafeTests(test_utils.TestCase): def test_run_coroutine_threadsafe(self): """Test coroutine submission from a thread to an event loop.""" - future = self.loop.run_in_executor(None, self.target) + future = self.loop.run_in_executor(None, self.target_coroutine) result = self.loop.run_until_complete(future) self.assertEqual(result, 3) def test_run_coroutine_threadsafe_with_exception(self): """Test coroutine submission from thread to event loop on exception.""" - future = self.loop.run_in_executor(None, self.target, True) + future = self.loop.run_in_executor(None, self.target_coroutine, True) with self.assertRaises(RuntimeError) as exc_context: self.loop.run_until_complete(future) self.assertIn("Fail!", exc_context.exception.args) + def test_run_coroutine_threadsafe_with_invalid(self): + """Test coroutine submission from thread to event loop on invalid.""" + callback = lambda: self.target_coroutine(invalid=True) # noqa + future = self.loop.run_in_executor(None, callback) + with self.assertRaises(ValueError) as exc_context: + self.loop.run_until_complete(future) + self.assertIn("Invalid!", exc_context.exception.args) + def test_run_coroutine_threadsafe_with_timeout(self): """Test coroutine submission from thread to event loop on timeout.""" - callback = lambda: self.target(timeout=0) # noqa + callback = lambda: self.target_coroutine(timeout=0) # noqa future = self.loop.run_in_executor(None, callback) with self.assertRaises(asyncio.TimeoutError): self.loop.run_until_complete(future) @@ -147,7 +192,28 @@ class RunCoroutineThreadsafeTests(test_utils.TestCase): def test_run_coroutine_threadsafe_task_cancelled(self): """Test coroutine submission from tread to event loop on cancel.""" - callback = lambda: self.target(cancel=True) # noqa + callback = lambda: self.target_coroutine(cancel=True) # noqa future = self.loop.run_in_executor(None, callback) with self.assertRaises(asyncio.CancelledError): self.loop.run_until_complete(future) + + def test_run_callback_threadsafe(self): + """Test callback submission from a thread to an event loop.""" + future = self.loop.run_in_executor(None, self.target_callback) + result = self.loop.run_until_complete(future) + self.assertEqual(result, 3) + + def test_run_callback_threadsafe_with_exception(self): + """Test callback submission from thread to event loop on exception.""" + future = self.loop.run_in_executor(None, self.target_callback, True) + with self.assertRaises(RuntimeError) as exc_context: + self.loop.run_until_complete(future) + self.assertIn("Fail!", exc_context.exception.args) + + def test_run_callback_threadsafe_with_invalid(self): + """Test callback submission from thread to event loop on invalid.""" + callback = lambda: self.target_callback(invalid=True) # noqa + future = self.loop.run_in_executor(None, callback) + with self.assertRaises(ValueError) as exc_context: + self.loop.run_until_complete(future) + self.assertIn("Invalid!", exc_context.exception.args) diff --git a/tests/util/test_init.py b/tests/util/test_init.py index ba8415d597f..2902cb62517 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,6 +1,6 @@ """Test Home Assistant util methods.""" import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock from datetime import datetime, timedelta from homeassistant import util @@ -266,3 +266,17 @@ class TestUtil(unittest.TestCase): self.assertTrue(tester.hello()) self.assertTrue(tester.goodbye()) + + @patch.object(util, 'random') + def test_get_random_string(self, mock_random): + """Test get random string.""" + results = ['A', 'B', 'C'] + + def mock_choice(choices): + return results.pop(0) + + generator = MagicMock() + generator.choice.side_effect = mock_choice + mock_random.SystemRandom.return_value = generator + + assert util.get_random_string(length=3) == 'ABC' diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py new file mode 100644 index 00000000000..94c8568dc47 --- /dev/null +++ b/tests/util/test_logging.py @@ -0,0 +1,68 @@ +"""Test Home Assistant logging util methods.""" +import asyncio +import logging +import threading + +import homeassistant.util.logging as logging_util + + +@asyncio.coroutine +def test_sensitive_data_filter(): + """Test the logging sensitive data filter.""" + log_filter = logging_util.HideSensitiveDataFilter('mock_sensitive') + + clean_record = logging.makeLogRecord({'msg': "clean log data"}) + log_filter.filter(clean_record) + assert clean_record.msg == "clean log data" + + sensitive_record = logging.makeLogRecord({'msg': "mock_sensitive log"}) + log_filter.filter(sensitive_record) + assert sensitive_record.msg == "******* log" + + +@asyncio.coroutine +def test_async_handler_loop_log(loop): + """Test the logging sensitive data filter.""" + loop._thread_ident = threading.get_ident() + + queue = asyncio.Queue(loop=loop) + base_handler = logging.handlers.QueueHandler(queue) + handler = logging_util.AsyncHandler(loop, base_handler) + + # Test passthrough props and noop functions + assert handler.createLock() is None + assert handler.acquire() is None + assert handler.release() is None + assert handler.formatter is base_handler.formatter + assert handler.name is base_handler.get_name() + handler.name = 'mock_name' + assert base_handler.get_name() == 'mock_name' + + log_record = logging.makeLogRecord({'msg': "Test Log Record"}) + handler.emit(log_record) + yield from handler.async_close(True) + assert queue.get_nowait() == log_record + assert queue.empty() + + +@asyncio.coroutine +def test_async_handler_thread_log(loop): + """Test the logging sensitive data filter.""" + loop._thread_ident = threading.get_ident() + + queue = asyncio.Queue(loop=loop) + base_handler = logging.handlers.QueueHandler(queue) + handler = logging_util.AsyncHandler(loop, base_handler) + + log_record = logging.makeLogRecord({'msg': "Test Log Record"}) + + def add_log(): + """Emit a mock log.""" + handler.emit(log_record) + handler.close() + + yield from loop.run_in_executor(None, add_log) + yield from handler.async_close(True) + + assert queue.get_nowait() == log_record + assert queue.empty()