Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/monitors_loader/monitors_loader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import importlib
import logging
import shutil
from datetime import datetime, timedelta
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions tests/components/monitors_loader/test_monitors_loader.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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),"
Expand All @@ -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)
Expand Down
67 changes: 63 additions & 4 deletions tests/module_loader/test_loader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib
import os
import sys
import time
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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"""
Expand Down
Loading