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(".moonraker.conf.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(".moonraker.conf.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(".moonraker.conf.bkp") bkp = confighelper.find_config_backup(str(cfg_path)) assert bkp == str(bkp_dest)