moonraker/tests/test_config.py

499 lines
19 KiB
Python

from __future__ import annotations
import pathlib
import pytest
import hashlib
import confighelper
import shutil
from confighelper import ConfigError
from moonraker import Server
from utils import ServerError
from components import gpio
from mocks import MockGpiod
from typing import TYPE_CHECKING, Dict
if TYPE_CHECKING:
from confighelper import ConfigHelper
@pytest.fixture(scope="class")
def config(base_server: Server) -> ConfigHelper:
base_server.load_component(base_server.config, "secrets")
return base_server.config
@pytest.fixture(scope="class")
def test_config(config: ConfigHelper,
path_args: Dict[str, pathlib.Path]
) -> ConfigHelper:
assets = path_args['asset_path']
sup_cfg_path = assets.joinpath("moonraker/supplemental.conf")
if not sup_cfg_path.exists():
pytest.fail("Supplemental config not found")
cfg = config.read_supplemental_config(str(sup_cfg_path))
return cfg["test_options"]
@pytest.fixture(scope="function")
def gpio_config(test_config: ConfigHelper,
monkeypatch: pytest.MonkeyPatch
) -> ConfigHelper:
def load_gpio_mock(name: str) -> MockGpiod:
return MockGpiod()
monkeypatch.setattr(gpio, "load_system_module", load_gpio_mock)
yield test_config
server = test_config.get_server()
gpio_comp = server.lookup_component("gpio", None)
if gpio_comp is not None:
gpio_comp.close()
gpio_comp.reserved_gpios = {}
class TestConfigGeneric:
def test_get_server(self, config: ConfigHelper):
server = config.get_server()
assert isinstance(server, Server)
def test_get_item(self, config: ConfigHelper):
sec = config["file_manager"]
assert sec.section == "file_manager"
def test_get_item_fail(self, config: ConfigHelper):
with pytest.raises(ConfigError):
config["not_available"]
def test_contains(self, config: ConfigHelper):
assert "file_manager" in config
def test_not_contains(self, config: ConfigHelper):
assert "not_available" not in config
def test_has_option(self, config: ConfigHelper):
assert config.has_option("host")
def test_get_name(self, config: ConfigHelper):
assert config.get_name() == "server"
def test_get_options(self,
config: ConfigHelper,
path_args: Dict[str, pathlib.Path]):
expected = {
"host": "0.0.0.0",
"port": "7010",
"ssl_port": "7011",
"klippy_uds_address": str(path_args["klippy_uds_path"])
}
assert expected == config.get_options()
def test_get_hash(self, config: ConfigHelper):
opts = config.get_options()
expected_hash = hashlib.sha256()
for opt, val in opts.items():
expected_hash.update(opt.encode())
expected_hash.update(val.encode())
cfg_hash = config.get_hash().hexdigest()
assert cfg_hash == expected_hash.hexdigest()
def test_missing_supplemental_config(config: ConfigHelper):
no_file = pathlib.Path("nofile")
with pytest.raises(ConfigError):
config.read_supplemental_config(no_file)
def test_error_supplemental_config(config: ConfigHelper,
path_args: Dict[str, pathlib.Path]):
assets = path_args["asset_path"]
invalid_cfg = assets.joinpath("moonraker/invalid_config.conf")
if not invalid_cfg.exists():
pytest.fail("Invalid Config File does not exist")
with pytest.raises(ConfigError):
config.read_supplemental_config(invalid_cfg)
def test_prefix_sections(test_config: ConfigHelper):
prefix = test_config.get_prefix_sections("prefix_sec")
expected = ["prefix_sec one", "prefix_sec two", "prefix_sec three"]
assert prefix == expected
class TestGetString:
def test_get_str_exists(self, test_config: ConfigHelper):
val = test_config.get("test_string")
assert val == "Hello World"
def test_get_st_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.get("invalid_option")
def test_get_st_default(self, test_config: ConfigHelper):
assert test_config.get("invalid_option", None) is None
def test_get_int_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.get("test_string", deprecate=True)
expected = (
f"[test_options]: Option 'test_string' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetInt:
def test_get_int_exists(self, test_config: ConfigHelper):
val = test_config.getint("test_int")
assert val == 1
def test_get_int_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getint("invalid_option")
def test_get_int_default(self, test_config: ConfigHelper):
assert test_config.getint("invalid_option", None) is None
def test_get_int_fail_above(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getint("test_int", above=1)
def test_get_int_fail_below(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getint("test_int", below=1)
def test_get_int_fail_minval(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getint("test_int", minval=2)
def test_get_int_fail_maxval(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getint("test_int", maxval=0)
def test_get_int_pass_all(self, test_config: ConfigHelper):
val = test_config.getint("test_int", above=0, below=2,
minval=1, maxval=1)
assert val == 1
def test_get_int_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.getint("test_int", deprecate=True)
expected = (
f"[test_options]: Option 'test_int' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetFloat:
def test_get_float_exists(self, test_config: ConfigHelper):
val = test_config.getfloat("test_float")
assert 3.5 == pytest.approx(val)
def test_get_float_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getfloat("invalid_option")
def test_get_float_default(self, test_config: ConfigHelper):
assert test_config.getfloat("invalid_option", None) is None
def test_get_float_fail_above(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getfloat("test_float", above=3.55)
def test_get_float_fail_below(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getfloat("test_float", below=3.45)
def test_get_float_fail_minval(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getfloat("test_float", minval=3.6)
def test_get_float_fail_maxval(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getfloat("test_float", maxval=3.45)
def test_get_float_pass_all(self, test_config: ConfigHelper):
val = test_config.getfloat("test_float", above=3.45, below=3.55,
minval=3, maxval=4)
assert 3.5 == pytest.approx(val)
def test_get_float_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.getfloat("test_float", deprecate=True)
expected = (
f"[test_options]: Option 'test_float' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetBoolean:
def test_get_boolean_exists(self, test_config: ConfigHelper):
val = test_config.getboolean("test_bool")
assert val is True
def test_get_float_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getboolean("invalid_option")
def test_get_float_default(self, test_config: ConfigHelper):
assert test_config.getboolean("invalid_option", None) is None
def test_get_int_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.getboolean("test_bool", deprecate=True)
expected = (
f"[test_options]: Option 'test_bool' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetList:
def test_get_list_exists(self, test_config: ConfigHelper):
val = test_config.getlist("test_list")
assert val == ["one", "two", "three"]
def test_get_list_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getlist("invalid_option")
def test_get_list_default(self, test_config: ConfigHelper):
assert test_config.getlist("invalid_option", None) is None
def test_get_int_list(self, test_config: ConfigHelper):
val = test_config.getintlist("test_int_list", separator=",")
assert val == [1, 2, 3]
def test_get_float_list(self, test_config: ConfigHelper):
val = test_config.getfloatlist("test_float_list", separator=",")
assert val == pytest.approx([1.5, 2.8, 3.2])
def test_get_multi_list(self, test_config: ConfigHelper):
val = test_config.getlists("test_multi_list", list_type=int,
separators=("\n", ","))
assert val == [[1, 2, 3], [4, 5, 6]]
def test_get_list_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.getlist("test_list", deprecate=True)
expected = (
f"[test_options]: Option 'test_list' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetDict:
def test_get_dict_exists(self, test_config: ConfigHelper):
val = test_config.getdict("test_dict", dict_type=int)
assert val == {"one": 1, "two": 2, "three": 3}
def test_get_dict_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getdict("invalid_option")
def test_get_dict_default(self, test_config: ConfigHelper):
assert test_config.getdict("invalid_option", None) is None
def test_get_dict_empty_fields(self, test_config: ConfigHelper):
val = test_config.getdict("test_dict_empty_field",
allow_empty_fields=True)
assert val == {"one": "test", "two": None, "three": None}
def test_get_dict_empty_fields_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.getdict("test_dict_empty_field")
def test_get_dict_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.getdict("test_dict", deprecate=True)
expected = (
f"[test_options]: Option 'test_dict' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetTemplate:
def test_get_template_exists(self, test_config: ConfigHelper):
val = test_config.gettemplate("test_template").render()
assert val == "mqttuser"
@pytest.mark.asyncio
async def test_get_template_async(self, test_config: ConfigHelper):
templ = test_config.gettemplate("test_template", is_async=True)
val = await templ.render_async()
assert val == "mqttuser"
def test_get_template_plain(self, test_config: ConfigHelper):
val = test_config.gettemplate("test_string").render()
assert val == "Hello World"
def test_get_template_fail(self, test_config: ConfigHelper):
with pytest.raises(ConfigError):
test_config.gettemplate("invalid_option")
def test_get_template_render_fail(self, test_config: ConfigHelper):
with pytest.raises(ServerError):
test_config.gettemplate("test_template", is_async=True).render()
def test_get_template_default(self, test_config: ConfigHelper):
assert test_config.gettemplate("invalid_option", None) is None
def test_load_template(self, test_config: ConfigHelper):
val = test_config.load_template("test_template").render()
assert val == "mqttuser"
def test_load_template_default(self, test_config: ConfigHelper):
templ = test_config.load_template(
"invalid_option", "{secrets.mqtt_credentials.password}")
val = templ.render()
assert val == "mqttpass"
def test_get_template_deprecate(self, test_config: ConfigHelper):
server = test_config.get_server()
test_config.gettemplate("test_template", deprecate=True)
expected = (
f"[test_options]: Option 'test_template' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetGpioOut:
def test_get_gpio_exists(self, gpio_config: ConfigHelper):
val: gpio.GpioOutputPin = gpio_config.getgpioout("test_gpio")
assert (
val.orig == "gpiochip0/gpio26" and
val.name == "gpiochip0:gpio26" and
val.inverted is False and
val.value == 0
)
def test_get_gpio_no_chip(self, gpio_config: ConfigHelper):
val: gpio.GpioOutputPin = gpio_config.getgpioout("test_gpio_no_chip")
assert (
val.orig == "gpio26" and
val.name == "gpiochip0:gpio26" and
val.inverted is False and
val.value == 0
)
def test_get_gpio_invert(self, gpio_config: ConfigHelper):
val: gpio.GpioOutputPin = gpio_config.getgpioout("test_gpio_invert")
assert (
val.orig == "!gpiochip0/gpio26" and
val.name == "gpiochip0:gpio26" and
val.inverted is True and
val.value == 0
)
def test_get_gpio_no_chip_invert(self, gpio_config: ConfigHelper):
val: gpio.GpioOutputPin = gpio_config.getgpioout(
"test_gpio_no_chip_invert")
assert (
val.orig == "!gpio26" and
val.name == "gpiochip0:gpio26" and
val.inverted is True and
val.value == 0
)
def test_get_gpio_initial_value(self, gpio_config: ConfigHelper):
val: gpio.GpioOutputPin = gpio_config.getgpioout(
"test_gpio", initial_value=1)
assert (
val.orig == "gpiochip0/gpio26" and
val.name == "gpiochip0:gpio26" and
val.inverted is False and
val.value == 1
)
def test_get_gpio_fail(self, gpio_config: ConfigHelper):
with pytest.raises(ConfigError):
gpio_config.getgpioout("invalid_option")
def test_get_gpio_default(self, gpio_config: ConfigHelper):
assert gpio_config.getgpioout("invalid_option", None) is None
@pytest.mark.parametrize("opt", ["pullup", "pullup_no_chip",
"pulldown", "pulldown_no_chip"])
def test_get_gpio_invalid(self, gpio_config: ConfigHelper, opt: str):
option = f"test_gpio_{opt}"
if not gpio_config.has_option(option):
pytest.fail(f"No option {option}")
with pytest.raises(ConfigError):
gpio_config.getgpioout(option)
def test_get_gpio_deprecated(self, gpio_config: ConfigHelper):
server = gpio_config.get_server()
gpio_config.getgpioout("test_gpio", deprecate=True)
expected = (
f"[test_options]: Option 'test_gpio' in is "
"deprecated, see the configuration documention "
"at https://moonraker.readthedocs.io"
)
assert expected in server.warnings
class TestGetConfiguration:
def test_get_config_no_exist(self, base_server: Server):
fake_path = pathlib.Path("no_exist")
if fake_path.exists():
pytest.fail("Path exists")
args = dict(base_server.app_args)
args["config_file"] = str(fake_path)
with pytest.raises(ConfigError):
confighelper.get_configuration(base_server, args)
def test_get_config_no_access(self,
base_server: Server,
path_args: Dict[str, pathlib.Path]
):
cfg_path = path_args["config_path"]
test_cfg = cfg_path.joinpath("test.conf")
shutil.copy(path_args["moonraker.conf"], test_cfg)
test_cfg.chmod(mode=222)
args = dict(base_server.app_args)
args["config_file"] = str(test_cfg)
with pytest.raises(ConfigError):
confighelper.get_configuration(base_server, args)
def test_get_config_no_server(self,
base_server: Server,
path_args: Dict[str, pathlib.Path]
):
assets = path_args['asset_path']
sup_cfg_path = assets.joinpath("moonraker/supplemental.conf")
if not sup_cfg_path.exists():
pytest.fail("Supplemental config not found")
args = dict(base_server.app_args)
args["config_file"] = str(sup_cfg_path)
with pytest.raises(ConfigError):
confighelper.get_configuration(base_server, args)
class TestBackupConfig:
def test_backup_fail(self, caplog: pytest.LogCaptureFixture):
fake_path = pathlib.Path("no_exist")
if fake_path.exists():
pytest.fail("Path exists")
confighelper.backup_config(fake_path)
assert "Failed to create a backup" == caplog.messages[-1]
def test_find_backup_fail(self):
fake_path = pathlib.Path("no_exist")
if fake_path.exists():
pytest.fail("Path exists")
result = confighelper.find_config_backup(fake_path)
assert result is None
def test_backup_config_success(self, path_args: Dict[str, pathlib.Path]):
cfg_path = path_args["moonraker.conf"]
bkp_dest = cfg_path.parent.joinpath(f".{cfg_path.name}.bkp")
if bkp_dest.exists():
pytest.fail("Backup Already Exists")
confighelper.backup_config(str(cfg_path))
assert bkp_dest.is_file()
def test_backup_skip(self, path_args: Dict[str, pathlib.Path]):
cfg_path = path_args["moonraker.conf"]
bkp_dest = cfg_path.parent.joinpath(f".{cfg_path.name}.bkp")
if not bkp_dest.exists():
pytest.fail("Backup Not Present")
stat = bkp_dest.stat()
confighelper.backup_config(str(cfg_path))
assert stat == bkp_dest.stat()
def test_find_backup(self, path_args: Dict[str, pathlib.Path]):
cfg_path = path_args["moonraker.conf"]
bkp_dest = cfg_path.parent.joinpath(f".{cfg_path.name}.bkp")
bkp = confighelper.find_config_backup(str(cfg_path))
assert bkp == str(bkp_dest)