Compare commits

..

3 Commits

Author SHA1 Message Date
John Lancaster
7a8e6f383a test updates 2024-09-04 23:50:57 -05:00
John Lancaster
b673bda1f2 formatting 2024-09-04 23:50:39 -05:00
John Lancaster
6c3ea5084b reworked family dependencies 2024-09-04 21:02:44 -05:00
13 changed files with 108 additions and 43 deletions

View File

@@ -1,20 +1,21 @@
child: child_app:
module: child module: child
class: Child class: Child
dependencies:
- parent_app
sibling: sibling_app:
module: sibling module: sibling
class: Sibling class: Sibling
dependencies:
- parent_app
parent: parent_app:
module: parent module: parent
class: Parent class: Parent
dependencies: dependencies:
- child - grand-parent_app
- sibling
grand-parent: grand-parent_app:
module: grand_parent module: grand_parent
class: GrandParent class: GrandParent
dependencies:
- parent

View File

@@ -1,5 +1,6 @@
from appdaemon.adapi import ADAPI from appdaemon.adapi import ADAPI
class Child(ADAPI): class Child(ADAPI):
def initialize(self): def initialize(self):
self.log(f"{self.__class__.__name__} Initialized") self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -1,5 +1,6 @@
from appdaemon.adapi import ADAPI from appdaemon.adapi import ADAPI
class GrandParent(ADAPI): class GrandParent(ADAPI):
def initialize(self): def initialize(self):
self.log(f"{self.__class__.__name__} Initialized") self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -1,5 +1,6 @@
from appdaemon.adapi import ADAPI from appdaemon.adapi import ADAPI
class Parent(ADAPI): class Parent(ADAPI):
def initialize(self): def initialize(self):
self.log(f"{self.__class__.__name__} Initialized") self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -2,6 +2,7 @@ from appdaemon.adapi import ADAPI
from child import Child from child import Child
class Sibling(Child): class Sibling(Child):
def initialize(self): def initialize(self):
self.log(f"{self.__class__.__name__} Initialized") self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -3,4 +3,4 @@ from appdaemon.adapi import ADAPI
class HelloWorld(ADAPI): class HelloWorld(ADAPI):
def initialize(self): def initialize(self):
self.log(f"{self.__class__.__name__} Initialized") self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -3,4 +3,4 @@ from appdaemon.adapi import ADAPI
class SimpleApp(ADAPI): class SimpleApp(ADAPI):
def initialize(self): def initialize(self):
self.log(f"{self.__class__.__name__} Initialized") self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -20,6 +20,9 @@ build-backend = "hatchling.build"
managed = true managed = true
dev-dependencies = [] dev-dependencies = []
[tool.ruff.format]
quote-style = "single"
[tool.hatch.metadata] [tool.hatch.metadata]
allow-direct-references = true allow-direct-references = true

View File

@@ -1,2 +1,2 @@
def hello() -> str: def hello() -> str:
return "Hello from ad-test!" return 'Hello from ad-test!'

View File

@@ -38,7 +38,7 @@ def base_config() -> AppDaemonConfig:
time_zone='America/Chicago', time_zone='America/Chicago',
config_dir=CONFIG_DIR, config_dir=CONFIG_DIR,
config_file='appdaemon.yaml', config_file='appdaemon.yaml',
module_debug={'_app_management': 'DEBUG'} module_debug={'_app_management': 'DEBUG'},
) )
@@ -48,6 +48,10 @@ def ad(base_config: AppDaemonConfig):
ad = AppDaemon(Logging(), loop, base_config) ad = AppDaemon(Logging(), loop, base_config)
import sys
sys.path.insert(0, (CONFIG_DIR / 'apps/food-repo/src').as_posix())
for cfg in ad.logging.config.values(): for cfg in ad.logging.config.values():
logger = logging.getLogger(cfg['name']) logger = logging.getLogger(cfg['name'])
logger.propagate = True logger.propagate = True

View File

@@ -3,16 +3,14 @@ import logging
import re import re
from pathlib import Path from pathlib import Path
import food.menu
import pytest import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
from git import Repo from git import Repo
from . import utils
from .utils import ( from .utils import (
count_error_lines,
get_app_orders, get_app_orders,
get_load_order, get_load_order,
get_loaded_apps,
reset_file, reset_file,
) )
@@ -20,6 +18,8 @@ INDENT = ' ' * 4
def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo): def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo):
import food.menu
module_file = Path(food.menu.__file__) module_file = Path(food.menu.__file__)
file_content = module_file.read_text() file_content = module_file.read_text()
@@ -40,7 +40,11 @@ def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
reset_file(config_repo, module_file) reset_file(config_repo, module_file)
def test_file_with_error(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo): def test_file_with_error(
ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
):
import food.menu
module_file = Path(food.menu.__file__) module_file = Path(food.menu.__file__)
file_content = module_file.read_text().splitlines() file_content = module_file.read_text().splitlines()
@@ -61,23 +65,38 @@ def test_file_with_error(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config
reset_file(config_repo, module_file) reset_file(config_repo, module_file)
def test_modification_child(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo): def test_modification_child(
ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
):
assert isinstance(ad, AppDaemon) assert isinstance(ad, AppDaemon)
ad.loop.run_until_complete(asyncio.sleep(2.5))
module_file = Path(__file__).parents[2] / 'conf/apps/family/child.py' module_file = Path(__file__).parents[2] / 'conf/apps/family/child.py'
file_content = module_file.read_text() utils.inject_into_file(module_file, 2, 'raise ImportError')
file_content += (INDENT * 2) + "self.log(f'Modified {self.__class__.__name__}')\n"
module_file.write_text(file_content)
try: try:
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
ad.loop.run_until_complete(asyncio.sleep(1.0)) ad.loop.run_until_complete(asyncio.sleep(1.0))
assert count_error_lines(caplog) == 0 assert "Error importing 'child'" in caplog.text
assert get_loaded_apps(caplog) == {'child'}
assert get_app_orders(caplog, 'stop') == ['child', 'parent', 'grand-parent'] load_order = utils.get_load_order(caplog)
assert get_app_orders(caplog, 'start') == ['child', 'parent', 'grand-parent'] assert load_order == ['child', 'sibling'] # sibling is a variant of child
stopping = get_app_orders(caplog, 'stop', ordered=False)
assert stopping is not None, 'Failed to get app stop order'
assert stopping == {'child_app', 'sibling_app'}
# start_order = get_app_orders(caplog, 'start')
# assert start_order is None
# assert start_order == ['child', 'parent', 'grand-parent'], f'Wrong app start order: {start_order}'
finally: finally:
reset_file(config_repo, module_file) reset_file(config_repo, module_file)
ad.loop.run_until_complete(asyncio.sleep(1.0)) ad.loop.run_until_complete(asyncio.sleep(1.0))
start_order = get_app_orders(caplog, 'start', ordered=False)
assert start_order == {
'child_app',
'sibling_app',
}, f'Wrong app start order: {start_order}'

View File

@@ -5,7 +5,7 @@ from typing import List
import pytest import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
from .utils import get_load_order, count_error_lines from .utils import count_error_lines, get_load_order
def validate_app_dependencies(ad: AppDaemon): def validate_app_dependencies(ad: AppDaemon):

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
import re import re
from pathlib import Path from pathlib import Path
from typing import List from typing import Generator, List, Literal
import pytest import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
@@ -13,23 +13,47 @@ async def delayed_stop(ad: AppDaemon, delay: float):
ad.stop() ad.stop()
def get_load_order(caplog: pytest.LogCaptureFixture) -> List[str]: def get_load_order(
for record in caplog.records: caplog: pytest.LogCaptureFixture, ordered: bool = True
if record.name == 'AppDaemon._app_management': ) -> Generator[str, None, None]:
if 'Determined module load order' in record.msg: records = (
return record.args[0] record.args[0]
for record in get_logger_records(caplog, 'AppDaemon._app_management')
if 'Determined module load order' in record.msg
)
try:
result = list(records)[-1]
if not ordered:
result = set(result)
return result
except Exception:
return
def get_logger_records(caplog: pytest.LogCaptureFixture, name: str): def get_logger_records(caplog: pytest.LogCaptureFixture, logger_name: str):
for record in caplog.records: for record in caplog.records:
if record.name == name: if record.name == logger_name:
yield record yield record
def get_app_orders(caplog: pytest.LogCaptureFixture, phase: str) -> List[str]: def get_app_orders(
for record in get_logger_records(caplog, 'AppDaemon._app_management'): caplog: pytest.LogCaptureFixture,
if re.search(f'App {phase} order', record.msg): phase: Literal['start', 'stop'],
return record.args[0] ordered: bool = True,
) -> list[str]:
"""Extracts the app start/stop order from the captured log lines"""
records = (
record.args[0]
for record in get_logger_records(caplog, 'AppDaemon._app_management')
if re.search(f'App {phase} order', record.msg)
)
try:
result = list(records)[-1]
if not ordered:
result = set(result)
return result
except Exception:
return
def get_loaded_apps(caplog: pytest.LogCaptureFixture): def get_loaded_apps(caplog: pytest.LogCaptureFixture):
@@ -39,7 +63,9 @@ def get_loaded_apps(caplog: pytest.LogCaptureFixture):
def count_error_lines(caplog: pytest.LogCaptureFixture) -> int: def count_error_lines(caplog: pytest.LogCaptureFixture) -> int:
error_log_lines = [msg for name, lvl, msg in caplog.record_tuples if name.startswith('Error')] error_log_lines = [
msg for name, lvl, msg in caplog.record_tuples if name.startswith('Error')
]
return len(error_log_lines) return len(error_log_lines)
@@ -47,3 +73,11 @@ def reset_file(repo: Repo, changed: Path):
if not changed.is_absolute(): if not changed.is_absolute():
changed = Path(repo.working_tree_dir) / changed changed = Path(repo.working_tree_dir) / changed
repo.git.checkout('HEAD', '--', changed) repo.git.checkout('HEAD', '--', changed)
def inject_into_file(file: Path, pos: int, line: str):
content = file.read_text()
lines = content.splitlines()
lines.insert(pos, line)
new_content = '\n'.join(lines)
file.write_text(new_content)