diff --git a/pyproject.toml b/pyproject.toml index 8b484c8..62a4d91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,9 @@ build-backend = "hatchling.build" managed = true dev-dependencies = [] +[tool.ruff.format] +quote-style = "single" + [tool.hatch.metadata] allow-direct-references = true diff --git a/src/ad_test/__init__.py b/src/ad_test/__init__.py index 8f990cf..63cea55 100644 --- a/src/ad_test/__init__.py +++ b/src/ad_test/__init__.py @@ -1,2 +1,2 @@ def hello() -> str: - return "Hello from ad-test!" + return 'Hello from ad-test!' diff --git a/src/ad_test/conftest.py b/src/ad_test/conftest.py index f06cedd..1ddfd5e 100644 --- a/src/ad_test/conftest.py +++ b/src/ad_test/conftest.py @@ -38,7 +38,7 @@ def base_config() -> AppDaemonConfig: time_zone='America/Chicago', config_dir=CONFIG_DIR, config_file='appdaemon.yaml', - module_debug={'_app_management': 'DEBUG'} + module_debug={'_app_management': 'DEBUG'}, ) @@ -47,7 +47,11 @@ def ad(base_config: AppDaemonConfig): loop = asyncio.new_event_loop() 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(): logger = logging.getLogger(cfg['name']) logger.propagate = True diff --git a/src/ad_test/test_file_change.py b/src/ad_test/test_file_change.py index 7db7ff1..59aaff2 100644 --- a/src/ad_test/test_file_change.py +++ b/src/ad_test/test_file_change.py @@ -3,16 +3,14 @@ import logging import re from pathlib import Path -import food.menu import pytest from appdaemon.appdaemon import AppDaemon from git import Repo +from . import utils from .utils import ( - count_error_lines, get_app_orders, get_load_order, - get_loaded_apps, reset_file, ) @@ -20,6 +18,8 @@ INDENT = ' ' * 4 def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo): + import food.menu + module_file = Path(food.menu.__file__) 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) -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__) 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) -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) + ad.loop.run_until_complete(asyncio.sleep(2.5)) module_file = Path(__file__).parents[2] / 'conf/apps/family/child.py' - file_content = module_file.read_text() - file_content += (INDENT * 2) + "self.log(f'Modified {self.__class__.__name__}')\n" - module_file.write_text(file_content) + utils.inject_into_file(module_file, 2, 'raise ImportError') try: with caplog.at_level(logging.DEBUG): ad.loop.run_until_complete(asyncio.sleep(1.0)) - assert count_error_lines(caplog) == 0 - assert get_loaded_apps(caplog) == {'child'} - assert get_app_orders(caplog, 'stop') == ['child', 'parent', 'grand-parent'] - assert get_app_orders(caplog, 'start') == ['child', 'parent', 'grand-parent'] + assert "Error importing 'child'" in caplog.text + + load_order = utils.get_load_order(caplog) + 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: reset_file(config_repo, module_file) 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}' diff --git a/src/ad_test/test_startup.py b/src/ad_test/test_startup.py index c0f6e02..5fc26a7 100644 --- a/src/ad_test/test_startup.py +++ b/src/ad_test/test_startup.py @@ -5,7 +5,7 @@ from typing import List import pytest 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): diff --git a/src/ad_test/utils.py b/src/ad_test/utils.py index 2a69681..748ac3a 100644 --- a/src/ad_test/utils.py +++ b/src/ad_test/utils.py @@ -1,7 +1,7 @@ import asyncio import re from pathlib import Path -from typing import List +from typing import Generator, List, Literal import pytest from appdaemon.appdaemon import AppDaemon @@ -13,23 +13,47 @@ async def delayed_stop(ad: AppDaemon, delay: float): ad.stop() -def get_load_order(caplog: pytest.LogCaptureFixture) -> List[str]: - for record in caplog.records: - if record.name == 'AppDaemon._app_management': - if 'Determined module load order' in record.msg: - return record.args[0] +def get_load_order( + caplog: pytest.LogCaptureFixture, ordered: bool = True +) -> Generator[str, None, None]: + records = ( + 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: - if record.name == name: + if record.name == logger_name: yield record -def get_app_orders(caplog: pytest.LogCaptureFixture, phase: str) -> List[str]: - for record in get_logger_records(caplog, 'AppDaemon._app_management'): - if re.search(f'App {phase} order', record.msg): - return record.args[0] +def get_app_orders( + caplog: pytest.LogCaptureFixture, + phase: Literal['start', 'stop'], + 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): @@ -39,7 +63,9 @@ def get_loaded_apps(caplog: pytest.LogCaptureFixture): 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) @@ -47,3 +73,11 @@ def reset_file(repo: Repo, changed: Path): if not changed.is_absolute(): changed = Path(repo.working_tree_dir) / 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)