123 lines
5.4 KiB
Python
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()
|