"""Verify debouncing when an expired AppDaemon timer is still queued.""" import sys import unittest from pathlib import Path from unittest.mock import Mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from smartcondition import Evaluator class FakeScheduler: def __init__(self): self.timers = {} self.next_id = 0 self.cancelled = [] self.warnings = [] self.expire_on_cancel = False self.queued = [] def run_in(self, callback, delay, **kwargs): self.next_id += 1 handle = str(self.next_id) self.timers[handle] = (callback, kwargs) return handle def timer_running(self, handle): return handle in self.timers def cancel_timer(self, handle, silent=False): self.cancelled.append(handle) if self.expire_on_cancel and handle in self.timers: self.queued.append(self.expire(handle)) cancelled = self.timers.pop(handle, None) is not None if not cancelled and not silent: self.warnings.append(handle) return cancelled def expire(self, handle): callback, kwargs = self.timers.pop(handle) return lambda: callback(kwargs) class DelayedCallbackTests(unittest.TestCase): def setUp(self): self.scheduler = FakeScheduler() self.evaluator = Evaluator.__new__(Evaluator) fields = dict( appdaemon_api=self.scheduler, callback_delay=10, trigger_callback_on_entity_creation=True, condition_state_change_delayed=[], condition_state_change_delayed_cb_handle=None, delay_generation=0, trigger_callbacks=Mock(), log=Mock(), ) for name, value in fields.items(): setattr(self.evaluator, '_Evaluator__' + name, value) def change(self, old, new): self.evaluator._Evaluator__on_condition_state_change( 'sensor.temperature', 'state', old, new, {}) return self.evaluator._Evaluator__condition_state_change_delayed_cb_handle def test_queued_obsolete_callback_preserves_new_timer_and_changes(self): old_handle = self.change('20', '21') queued_callback = self.scheduler.expire(old_handle) new_handle = self.change('21', '22') queued_callback() self.assertEqual(self.scheduler.warnings, []) self.assertEqual(self.evaluator._Evaluator__condition_state_change_delayed_cb_handle, new_handle) self.assertEqual(len(self.evaluator._Evaluator__condition_state_change_delayed), 2) self.evaluator._Evaluator__trigger_callbacks.assert_not_called() self.evaluator._Evaluator__log.assert_not_called() self.scheduler.expire(new_handle)() self.evaluator._Evaluator__trigger_callbacks.assert_called_once() reason = self.evaluator._Evaluator__trigger_callbacks.call_args.args[0] self.assertIn('20 => 21', reason) self.assertIn('21 => 22', reason) self.assertEqual(self.evaluator._Evaluator__condition_state_change_delayed, []) self.assertIsNone(self.evaluator._Evaluator__condition_state_change_delayed_cb_handle) self.evaluator._Evaluator__log.assert_not_called() def test_obsolete_callback_after_current_callback_is_ignored(self): queued_callback = self.scheduler.expire(self.change('20', '21')) self.scheduler.expire(self.change('21', '22'))() queued_callback() self.evaluator._Evaluator__trigger_callbacks.assert_called_once() self.evaluator._Evaluator__log.assert_not_called() def test_running_timer_is_cancelled_when_rescheduled(self): old_handle = self.change('20', '21') new_handle = self.change('21', '22') self.assertEqual(self.scheduler.cancelled, [old_handle]) self.assertFalse(self.scheduler.timer_running(old_handle)) self.scheduler.expire(new_handle)() self.evaluator._Evaluator__trigger_callbacks.assert_called_once() def test_expiry_during_cancellation_is_silent_and_preserves_new_batch(self): self.change('20', '21') self.scheduler.expire_on_cancel = True current = self.change('21', '22') self.assertEqual(self.scheduler.warnings, []) self.assertEqual(len(self.scheduler.queued), 1) self.scheduler.queued[0]() self.evaluator._Evaluator__trigger_callbacks.assert_not_called() self.assertEqual(self.evaluator._Evaluator__condition_state_change_delayed_cb_handle, current) self.assertEqual(len(self.evaluator._Evaluator__condition_state_change_delayed), 2) self.scheduler.expire(current)() self.evaluator._Evaluator__trigger_callbacks.assert_called_once() self.evaluator._Evaluator__log.assert_not_called() def test_zero_delay_remains_immediate(self): self.evaluator._Evaluator__callback_delay = 0 self.change('20', '21') self.evaluator._Evaluator__trigger_callbacks.assert_called_once() self.assertEqual(self.scheduler.timers, {}) if __name__ == '__main__': unittest.main()