Files
ad_toolbox/unit_tests/test_logger_interface_grouping.py
T

123 lines
5.4 KiB
Python

"""Verify scoped log grouping without replacing AppDaemon API methods."""
import os
import pickle
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
TOOLBOX = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(TOOLBOX))
from logger_interface import LoggerInterface
import smartcondition
def load_smart_object():
source = (TOOLBOX / 'smartobject.py').read_text()
import ast
tree = ast.parse(source)
smart_object = next(node for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == 'SmartObject')
class FakeHass:
def log(self, message, level='INFO', log=None):
self.rows.append((level, message, log))
namespace = {'hass': SimpleNamespace(Hass=FakeHass),
'LoggerInterface': LoggerInterface, 'VirtualSensors': object,
'ParsingException': type('ParsingException', (Exception,), {}),
'pickle': pickle, 'os': os}
exec(compile(ast.Module(body=[smart_object], type_ignores=[]), str(TOOLBOX / 'smartobject.py'), 'exec'), namespace)
return namespace['SmartObject']
class FakeApp(LoggerInterface):
name = 'fake_app'
def __init__(self):
self.rows = []
super().__init__(self)
def log(self, message, level='INFO', log=None):
self.rows.append((level, message, log))
def initialize(self):
self.log_info('LoggerInterface initialization message')
self.log('Direct app log stays readable')
class LoggerGroupingTests(unittest.TestCase):
def test_initialize_is_not_implicitly_wrapped_or_tagged(self):
app = FakeApp()
original_initialize = app.initialize.__func__
original_log = app.log.__func__
app.initialize()
self.assertIs(app.initialize.__func__, original_initialize)
self.assertIs(app.log.__func__, original_log)
self.assertEqual(app.rows[0][1], 'LoggerInterface initialization message')
self.assertEqual(app.rows[1][1], 'Direct app log stays readable')
def test_smart_object_groups_its_own_lifecycle(self):
SmartObject = load_smart_object()
app = SmartObject.__new__(SmartObject)
app.rows = []
app.name = 'test_smart_object'
app.args = {}
with tempfile.TemporaryDirectory() as folder:
app.AD = SimpleNamespace(app_dir=folder)
app.get_ad_api = lambda: app
app.on_initialize_smart_object = lambda: app.log_info('Subclass initialization hook')
app.initialize()
self.assertIn('Initialization started [group=initialization:', app.rows[0][1])
group_id = app.rows[0][1].split(':', 1)[1].rstrip(']')
self.assertIn(f'[group=initialization:{group_id}]', app.rows[1][1])
self.assertIn(f'[group=initialization:{group_id}]', app.rows[2][1])
self.assertEqual(app.rows[-1][1], f'Initialization completed [group=initialization:{group_id}]')
def test_smartcondition_uses_smartobject_api_scope_without_logger_interface_argument(self):
app = FakeApp()
evaluator = smartcondition.Evaluator.__new__(smartcondition.Evaluator)
evaluator._Evaluator__logger_interface = None
evaluator._Evaluator__appdaemon_api = app
evaluator._Evaluator__log_group = 'smart_conditions'
with evaluator._Evaluator__log_group_scope():
app.log_info('Callback trigger by sensor.example(0 => 1)')
app.log_info('[smart_conditions][Evaluating] Triggered by condition')
marker_a = app.rows[0][1].split('[group=')[1].rstrip(']')
marker_b = app.rows[1][1].split('[group=')[1].rstrip(']')
self.assertEqual(marker_a, marker_b)
self.assertTrue(marker_a.startswith('smartcondition:'))
def test_log_group_takes_priority_over_condition_name(self):
app = FakeApp()
evaluator = smartcondition.Evaluator(app, {}, activated=False,
condition_name='condition-a', log_group='group-a')
self.assertEqual(evaluator._Evaluator__log_group, 'group-a')
self.assertEqual(evaluator._Evaluator__log_to_string('message'), '[condition-a] message')
with evaluator._Evaluator__log_group_scope() as group:
self.assertEqual(group['label'], 'group-a')
fallback = smartcondition.Evaluator(app, {}, activated=False,
condition_name=['condition-a', 'condition-b'])
self.assertEqual(fallback._Evaluator__log_group, 'condition-a, condition-b')
def test_nested_scopes_inherit_outer_group_and_reset_after_exception(self):
app = FakeApp()
with app.log_group('value_selector', 'sensor.a') as outer:
app.log_info('candidate rejected')
with app.log_group('smartcondition', 'condition-a') as inner:
self.assertIs(inner, outer)
app.log_info('condition result')
app.log_info('outside group')
self.assertIn(f'[group=value_selector:{outer["id"]}]', app.rows[0][1])
self.assertIn(f'[group=value_selector:{outer["id"]}]', app.rows[1][1])
self.assertEqual(app.rows[2][1], 'outside group')
with self.assertRaises(RuntimeError):
with app.log_group('smartcondition'):
raise RuntimeError('expected')
app.log_info('after failure')
self.assertEqual(app.rows[3][1], 'after failure')
if __name__ == '__main__':
unittest.main()