2024-11-10 23:49:10 +01:00
|
|
|
import logging
|
2024-12-30 13:41:39 +01:00
|
|
|
import os
|
2024-10-08 14:29:45 +02:00
|
|
|
import subprocess
|
|
|
|
import sys
|
2024-12-15 14:40:03 +01:00
|
|
|
import tempfile
|
2024-11-11 21:38:13 +01:00
|
|
|
from pathlib import Path
|
2024-12-15 14:40:03 +01:00
|
|
|
from typing import Optional
|
2024-12-30 13:41:39 +01:00
|
|
|
from unittest.mock import PropertyMock, patch
|
2024-10-08 14:29:45 +02:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
import pendulum
|
2024-10-08 14:29:45 +02:00
|
|
|
import pytest
|
|
|
|
from xprocess import ProcessStarter
|
|
|
|
|
2024-12-30 13:41:39 +01:00
|
|
|
from akkudoktoreos.config.config import ConfigEOS, get_config
|
2025-01-05 14:41:07 +01:00
|
|
|
from akkudoktoreos.core.logging import get_logger
|
2024-11-11 21:38:13 +01:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
logger = get_logger(__name__)
|
2024-11-11 21:38:13 +01:00
|
|
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
@pytest.fixture()
|
|
|
|
def disable_debug_logging(scope="session", autouse=True):
|
|
|
|
"""Automatically disable debug logging for all tests."""
|
|
|
|
original_levels = {}
|
|
|
|
root_logger = logging.getLogger()
|
|
|
|
|
|
|
|
original_levels[root_logger] = root_logger.level
|
|
|
|
root_logger.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
for logger_name, logger in logging.root.manager.loggerDict.items():
|
|
|
|
if isinstance(logger, logging.Logger):
|
|
|
|
original_levels[logger] = logger.level
|
|
|
|
if logger.level <= logging.DEBUG:
|
|
|
|
logger.setLevel(logging.INFO)
|
2024-10-08 14:29:45 +02:00
|
|
|
|
2024-11-10 23:49:10 +01:00
|
|
|
yield
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
for logger, level in original_levels.items():
|
|
|
|
logger.setLevel(level)
|
2024-11-10 23:49:10 +01:00
|
|
|
|
|
|
|
|
2024-11-10 23:22:30 +01:00
|
|
|
def pytest_addoption(parser):
|
|
|
|
parser.addoption(
|
|
|
|
"--full-run", action="store_true", default=False, help="Run with all optimization tests."
|
|
|
|
)
|
2024-12-30 13:41:39 +01:00
|
|
|
parser.addoption(
|
|
|
|
"--check-config-side-effect",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="Verify that user config file is non-existent (will also fail if user config file exists before test run).",
|
|
|
|
)
|
2024-11-10 23:22:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def is_full_run(request):
|
|
|
|
yield bool(request.config.getoption("--full-run"))
|
|
|
|
|
|
|
|
|
2024-12-30 13:41:39 +01:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def config_mixin(config_eos):
|
|
|
|
with patch(
|
|
|
|
"akkudoktoreos.core.coreabc.ConfigMixin.config", new_callable=PropertyMock
|
|
|
|
) as config_mixin_patch:
|
|
|
|
config_mixin_patch.return_value = config_eos
|
|
|
|
yield config_mixin_patch
|
|
|
|
|
|
|
|
|
|
|
|
# Test if test has side effect of writing to system (user) config file
|
|
|
|
# Before activating, make sure that no user config file exists (e.g. ~/.config/net.akkudoktoreos.eos/EOS.config.json)
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def cfg_non_existent(request):
|
|
|
|
yield
|
|
|
|
if bool(request.config.getoption("--check-config-side-effect")):
|
|
|
|
from platformdirs import user_config_dir
|
|
|
|
|
|
|
|
user_dir = user_config_dir(ConfigEOS.APP_NAME)
|
|
|
|
assert not Path(user_dir).joinpath(ConfigEOS.CONFIG_FILE_NAME).exists()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def user_config_dir(config_default_dirs):
|
|
|
|
with patch(
|
|
|
|
"akkudoktoreos.config.config.user_config_dir",
|
|
|
|
return_value=str(config_default_dirs[0]),
|
|
|
|
) as user_dir_patch:
|
|
|
|
yield user_dir_patch
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def user_data_dir(config_default_dirs):
|
|
|
|
with patch(
|
|
|
|
"akkudoktoreos.config.config.user_data_dir",
|
|
|
|
return_value=str(config_default_dirs[-1] / "data"),
|
|
|
|
) as user_dir_patch:
|
|
|
|
yield user_dir_patch
|
|
|
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
@pytest.fixture
|
2024-12-30 13:41:39 +01:00
|
|
|
def config_eos(
|
|
|
|
disable_debug_logging,
|
|
|
|
user_config_dir,
|
|
|
|
user_data_dir,
|
|
|
|
config_default_dirs,
|
|
|
|
monkeypatch,
|
|
|
|
) -> ConfigEOS:
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Fixture to reset EOS config to default values."""
|
2024-12-30 13:41:39 +01:00
|
|
|
monkeypatch.setenv("data_cache_subpath", str(config_default_dirs[-1] / "data/cache"))
|
|
|
|
monkeypatch.setenv("data_output_subpath", str(config_default_dirs[-1] / "data/output"))
|
|
|
|
config_file = config_default_dirs[0] / ConfigEOS.CONFIG_FILE_NAME
|
|
|
|
assert not config_file.exists()
|
2024-12-15 14:40:03 +01:00
|
|
|
config_eos = get_config()
|
|
|
|
config_eos.reset_settings()
|
2024-12-30 13:41:39 +01:00
|
|
|
assert config_file == config_eos.config_file_path
|
|
|
|
assert config_file.exists()
|
|
|
|
assert config_default_dirs[-1] / "data" == config_eos.data_folder_path
|
|
|
|
assert config_default_dirs[-1] / "data/cache" == config_eos.data_cache_path
|
|
|
|
assert config_default_dirs[-1] / "data/output" == config_eos.data_output_path
|
2024-12-15 14:40:03 +01:00
|
|
|
return config_eos
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def config_default_dirs():
|
|
|
|
"""Fixture that provides a list of directories to be used as config dir."""
|
2024-12-30 13:41:39 +01:00
|
|
|
with tempfile.TemporaryDirectory() as tmp_user_home_dir:
|
|
|
|
# Default config directory from platform user config directory
|
|
|
|
config_default_dir_user = Path(tmp_user_home_dir) / "config"
|
|
|
|
# Default config directory from current working directory
|
|
|
|
config_default_dir_cwd = Path.cwd()
|
|
|
|
# Default config directory from default config file
|
|
|
|
config_default_dir_default = Path(__file__).parent.parent.joinpath("src/akkudoktoreos/data")
|
|
|
|
|
|
|
|
# Default data directory from platform user data directory
|
|
|
|
data_default_dir_user = Path(tmp_user_home_dir)
|
|
|
|
yield (
|
|
|
|
config_default_dir_user,
|
|
|
|
config_default_dir_cwd,
|
|
|
|
config_default_dir_default,
|
|
|
|
data_default_dir_user,
|
|
|
|
)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2024-12-30 13:41:39 +01:00
|
|
|
def server(xprocess, config_eos, config_default_dirs):
|
2024-11-10 23:49:10 +01:00
|
|
|
"""Fixture to start the server.
|
|
|
|
|
|
|
|
Provides URL of the server.
|
|
|
|
"""
|
|
|
|
|
2024-10-08 14:29:45 +02:00
|
|
|
class Starter(ProcessStarter):
|
2024-12-30 13:41:39 +01:00
|
|
|
# Set environment before any subprocess run, to keep custom config dir
|
|
|
|
env = os.environ.copy()
|
|
|
|
env["EOS_DIR"] = str(config_default_dirs[-1])
|
|
|
|
|
2024-10-08 14:29:45 +02:00
|
|
|
# assure server to be installed
|
|
|
|
try:
|
|
|
|
subprocess.run(
|
2024-12-15 14:40:03 +01:00
|
|
|
[sys.executable, "-c", "import akkudoktoreos.server.fastapi_server"],
|
2024-10-08 14:29:45 +02:00
|
|
|
check=True,
|
2024-12-30 13:41:39 +01:00
|
|
|
env=env,
|
2024-10-08 14:29:45 +02:00
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError:
|
2025-01-05 14:41:07 +01:00
|
|
|
project_dir = config_eos.package_root_path
|
2024-10-08 14:29:45 +02:00
|
|
|
subprocess.run(
|
|
|
|
[sys.executable, "-m", "pip", "install", "-e", project_dir],
|
|
|
|
check=True,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
)
|
|
|
|
|
|
|
|
# command to start server process
|
2024-11-15 22:27:25 +01:00
|
|
|
args = [sys.executable, "-m", "akkudoktoreos.server.fastapi_server"]
|
2024-10-08 14:29:45 +02:00
|
|
|
|
|
|
|
# startup pattern
|
2024-11-15 22:27:25 +01:00
|
|
|
pattern = "Application startup complete."
|
2024-12-15 14:40:03 +01:00
|
|
|
# search this number of lines for the startup pattern, if not found
|
2024-10-08 14:29:45 +02:00
|
|
|
# a RuntimeError will be raised informing the user
|
2024-11-11 21:38:13 +01:00
|
|
|
max_read_lines = 30
|
2024-10-08 14:29:45 +02:00
|
|
|
|
2024-11-11 21:38:13 +01:00
|
|
|
# will wait for 30 seconds before timing out
|
2024-10-10 12:59:33 +02:00
|
|
|
timeout = 30
|
2024-10-08 14:29:45 +02:00
|
|
|
|
|
|
|
# xprocess will now attempt to clean up upon interruptions
|
|
|
|
terminate_on_interrupt = True
|
|
|
|
|
|
|
|
# ensure process is running and return its logfile
|
2024-12-15 14:40:03 +01:00
|
|
|
pid, logfile = xprocess.ensure("eos", Starter)
|
2024-12-30 13:41:39 +01:00
|
|
|
print(f"View xprocess logfile at: {logfile}")
|
2024-10-08 14:29:45 +02:00
|
|
|
|
|
|
|
# create url/port info to the server
|
|
|
|
url = "http://127.0.0.1:8503"
|
|
|
|
yield url
|
|
|
|
|
|
|
|
# clean up whole process tree afterwards
|
2024-11-15 22:27:25 +01:00
|
|
|
xprocess.getinfo("eos").terminate()
|
2024-11-10 23:49:10 +01:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2024-12-15 14:40:03 +01:00
|
|
|
def set_other_timezone():
|
|
|
|
"""Temporarily sets a timezone for Pendulum during a test.
|
2024-11-10 23:49:10 +01:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
Resets to the original timezone after the test completes.
|
2024-11-10 23:49:10 +01:00
|
|
|
"""
|
2024-12-15 14:40:03 +01:00
|
|
|
original_timezone = pendulum.local_timezone()
|
2024-11-10 23:49:10 +01:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
default_other_timezone = "Atlantic/Canary"
|
|
|
|
if default_other_timezone == original_timezone:
|
|
|
|
default_other_timezone = "Asia/Singapore"
|
2024-11-10 23:49:10 +01:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
def _set_timezone(other_timezone: Optional[str] = None) -> str:
|
|
|
|
if other_timezone is None:
|
|
|
|
other_timezone = default_other_timezone
|
|
|
|
pendulum.set_local_timezone(other_timezone)
|
|
|
|
assert pendulum.local_timezone() == other_timezone
|
|
|
|
return other_timezone
|
2024-11-10 23:49:10 +01:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
yield _set_timezone
|
2024-11-10 23:49:10 +01:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
# Restore the original timezone
|
|
|
|
pendulum.set_local_timezone(original_timezone)
|
|
|
|
assert pendulum.local_timezone() == original_timezone
|