diff --git a/src/components/monitors_loader/monitors_loader.py b/src/components/monitors_loader/monitors_loader.py index 22cdfc23..3687507f 100644 --- a/src/components/monitors_loader/monitors_loader.py +++ b/src/components/monitors_loader/monitors_loader.py @@ -1,4 +1,5 @@ import asyncio +import importlib import logging import shutil from datetime import datetime, timedelta @@ -221,6 +222,8 @@ async def _load_monitors() -> None: additional_files=code_module.additional_files, ) + importlib.invalidate_caches() + for monitor in loaded_monitors: with catch_exceptions(_logger): monitor_path = monitors_paths.get(monitor.id) diff --git a/tests/components/monitors_loader/test_monitors_loader.py b/tests/components/monitors_loader/test_monitors_loader.py index ef9dcdfd..795f8067 100644 --- a/tests/components/monitors_loader/test_monitors_loader.py +++ b/tests/components/monitors_loader/test_monitors_loader.py @@ -1,9 +1,10 @@ import asyncio +import importlib import sys from datetime import datetime, timezone from pathlib import Path from types import ModuleType -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest @@ -537,9 +538,11 @@ def reactions_list(self): assert monitor_module.reaction_options.alert_solved == [do_nothing, "do_nothing"] -async def test_load_monitors(clear_database): +async def test_load_monitors(mocker, clear_database): """'_load_monitors' should load all enabled monitors from the database and add them to the registry""" + invalidate_caches_spy: MagicMock = mocker.spy(importlib, "invalidate_caches") + await databases.execute_application( 'insert into "Monitors"(id, name, enabled) values' "(9999123, 'monitor_1', true)," @@ -555,6 +558,8 @@ async def test_load_monitors(clear_database): await monitors_loader._load_monitors() + invalidate_caches_spy.assert_called_once() + assert len(registry._monitors) == 2 assert isinstance(registry._monitors[9999123]["module"], ModuleType) assert isinstance(registry._monitors[9999456]["module"], ModuleType) diff --git a/tests/module_loader/test_loader.py b/tests/module_loader/test_loader.py index e067080a..e34cad0e 100644 --- a/tests/module_loader/test_loader.py +++ b/tests/module_loader/test_loader.py @@ -1,3 +1,4 @@ +import importlib import os import sys import time @@ -134,10 +135,44 @@ def test_load_module_from_file(caplog): (10, 200), ], ) -def test_load_module_from_file_reload(caplog, n1, n2): +def test_load_module_from_file_reload_invalidate_cache(caplog, n1, n2): """'load_module_from_file' should be able to reload modules that were previously loaded, - allowing hot changes""" - module_name = f"load_module_from_file_reload_{n1}_{n2}" + allowing hot changes. + To be able reload a file the file timestamp or size must change, or the cache must be + invalidated""" + module_name = f"test_load_module_from_file_reload_invalidate_cache_{n1}_{n2}" + module_code = "def get_value(): return {n}" + + module_path = loader.create_module_files(module_name, module_code.format(n=n1)) + + module = loader.load_module_from_file(module_path) + + assert module.get_value() == n1 + assert_message_in_log(caplog, f"Monitor '{module_name}' loaded") + + importlib.invalidate_caches() + + module_path = loader.create_module_files(module_name, module_code.format(n=n2)) + + module = loader.load_module_from_file(module_path) + + assert module.get_value() == n2 + assert_message_in_log(caplog, f"Monitor '{module_name}' loaded", count=2) + + +@pytest.mark.parametrize( + "n1, n2", + [ + (10, 99), # In this test case, the file size doesn't change + (10, 200), + ], +) +def test_load_module_from_file_reload_sleep(caplog, n1, n2): + """'load_module_from_file' should be able to reload modules that were previously loaded, + allowing hot changes. + To be able reload a file the file timestamp or size must change, or the cache must be + invalidated""" + module_name = f"test_load_module_from_file_reload_sleep_{n1}_{n2}" module_code = "def get_value(): return {n}" module_path = loader.create_module_files(module_name, module_code.format(n=n1)) @@ -147,7 +182,6 @@ def test_load_module_from_file_reload(caplog, n1, n2): assert module.get_value() == n1 assert_message_in_log(caplog, f"Monitor '{module_name}' loaded") - # As python checks for the timestamp to change to reload a module, sleep for 1 second time.sleep(1) module_path = loader.create_module_files(module_name, module_code.format(n=n2)) @@ -158,6 +192,31 @@ def test_load_module_from_file_reload(caplog, n1, n2): assert_message_in_log(caplog, f"Monitor '{module_name}' loaded", count=2) +@pytest.mark.flaky(reruns=1) +def test_load_module_from_file_reload_no_time_change(caplog): + """'load_module_from_file' should not reload the file if the file timestamp or size didn't + change.""" + module_name = "test_load_module_from_file_reload_no_time_change" + module_code = "def get_value(): return {n}" + + module_path = loader.create_module_files(module_name, module_code.format(n=10)) + + module = loader.load_module_from_file(module_path) + + assert module.get_value() == 10 + assert_message_in_log(caplog, f"Monitor '{module_name}' loaded") + + time.sleep(0.1) + + module_path = loader.create_module_files(module_name, module_code.format(n=99)) + + module = loader.load_module_from_file(module_path) + + # The value didn't change, even though the module code changed + assert module.get_value() == 10 + assert_message_in_log(caplog, f"Monitor '{module_name}' loaded", count=2) + + def test_load_module_from_file_reload_replace_variables(): """'load_module_from_file' should be able to reload modules replacing the previous state for a new one"""