Coverage for mlos_bench/mlos_bench/tests/config/cli/test_load_cli_config_examples.py: 97%
58 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-21 01:50 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-21 01:50 +0000
1#
2# Copyright (c) Microsoft Corporation.
3# Licensed under the MIT License.
4#
5"""Tests for loading storage config examples."""
7import logging
8from importlib.resources import files
10import pytest
12from mlos_bench.config.schemas import ConfigSchema
13from mlos_bench.environments import Environment
14from mlos_bench.launcher import Launcher
15from mlos_bench.optimizers import Optimizer
16from mlos_bench.schedulers import Scheduler
17from mlos_bench.services.config_persistence import ConfigPersistenceService
18from mlos_bench.storage import Storage
19from mlos_bench.tests import check_class_name
20from mlos_bench.tests.config import BUILTIN_TEST_CONFIG_PATH, locate_config_examples
21from mlos_bench.util import path_join
23_LOG = logging.getLogger(__name__)
24_LOG.setLevel(logging.DEBUG)
27# Get the set of configs to test.
28CONFIG_TYPE = "cli"
31def filter_configs(configs_to_filter: list[str]) -> list[str]:
32 """If necessary, filter out json files that aren't for the module we're testing."""
33 return configs_to_filter
36configs = [
37 *locate_config_examples(
38 ConfigPersistenceService.BUILTIN_CONFIG_PATH,
39 CONFIG_TYPE,
40 filter_configs,
41 ),
42 *locate_config_examples(
43 BUILTIN_TEST_CONFIG_PATH,
44 CONFIG_TYPE,
45 filter_configs,
46 ),
47]
48assert configs
51@pytest.mark.skip(reason="Use full Launcher test (below) instead now.")
52@pytest.mark.parametrize("config_path", configs)
53def test_load_cli_config_examples(
54 config_loader_service: ConfigPersistenceService,
55 config_path: str,
56) -> None: # pragma: no cover
57 """Tests loading a config example."""
58 # pylint: disable=too-complex
59 config = config_loader_service.load_config(config_path, ConfigSchema.CLI)
60 assert isinstance(config, dict)
62 if config_paths := config.get("config_path"):
63 assert isinstance(config_paths, list)
64 config_paths.reverse()
65 for path in config_paths:
66 config_loader_service._config_path.insert(0, path) # pylint: disable=protected-access
68 # Foreach arg that references another file, see if we can at least load that too.
69 args_to_skip = {
70 "config_path", # handled above
71 "log_file",
72 "log_level",
73 "experiment_id",
74 "trial_id",
75 "teardown",
76 }
77 for arg in config:
78 if arg in args_to_skip:
79 continue
81 if arg == "globals":
82 for path in config[arg]:
83 sub_config = config_loader_service.load_config(path, ConfigSchema.GLOBALS)
84 assert isinstance(sub_config, dict)
85 elif arg == "environment":
86 sub_config = config_loader_service.load_config(config[arg], ConfigSchema.ENVIRONMENT)
87 assert isinstance(sub_config, dict)
88 elif arg == "optimizer":
89 sub_config = config_loader_service.load_config(config[arg], ConfigSchema.OPTIMIZER)
90 assert isinstance(sub_config, dict)
91 elif arg == "storage":
92 sub_config = config_loader_service.load_config(config[arg], ConfigSchema.STORAGE)
93 assert isinstance(sub_config, dict)
94 elif arg == "tunable_values":
95 for path in config[arg]:
96 sub_config = config_loader_service.load_config(path, ConfigSchema.TUNABLE_VALUES)
97 assert isinstance(sub_config, dict)
98 else:
99 raise NotImplementedError(f"Unhandled arg {arg} in config {config_path}")
102@pytest.mark.parametrize("config_path", configs)
103def test_load_cli_config_examples_via_launcher(
104 config_loader_service: ConfigPersistenceService,
105 config_path: str,
106) -> None:
107 """Tests loading a config example via the Launcher."""
108 config = config_loader_service.load_config(config_path, ConfigSchema.CLI)
109 assert isinstance(config, dict)
111 # Try to load the CLI config by instantiating a launcher.
112 # To do this we need to make sure to give it a few extra paths and globals
113 # to look for for our examples.
114 cli_args = (
115 # pylint: disable=inconsistent-quotes
116 f"--config {config_path}"
117 f" --config-path {files('mlos_bench.config')} "
118 f" --config-path {files('mlos_bench.tests.config')}"
119 f" --config-path {path_join(str(files('mlos_bench.tests.config')), 'globals')}"
120 f" --globals {files('mlos_bench.tests.config')}/experiments/experiment_test_config.jsonc"
121 )
122 launcher = Launcher(description=__name__, long_text=config_path, argv=cli_args.split())
123 assert launcher
125 # Check that some parts of that config are loaded.
127 assert ConfigPersistenceService.BUILTIN_CONFIG_PATH in launcher.config_loader.config_paths
128 if config_paths := config.get("config_path"):
129 assert isinstance(config_paths, list)
130 for path in config_paths:
131 # Note: Checks that the order is maintained are handled in launcher_parse_args.py
132 assert any(
133 config_path.endswith(path) for config_path in launcher.config_loader.config_paths
134 ), f"Expected {path} to be in {launcher.config_loader.config_paths}"
136 if "experiment_id" in config:
137 assert launcher.global_config["experiment_id"] == config["experiment_id"]
138 if "trial_id" in config:
139 assert launcher.global_config["trial_id"] == config["trial_id"]
141 expected_log_level = logging.getLevelName(config.get("log_level", "INFO"))
142 if isinstance(expected_log_level, int): # type: ignore[unreachable]
143 expected_log_level = logging.getLevelName(expected_log_level) # type: ignore[unreachable]
144 current_log_level = logging.getLevelName(logging.root.getEffectiveLevel())
145 assert current_log_level == expected_log_level
147 # TODO: Check that the log_file handler is set correctly.
149 expected_teardown = config.get("teardown", True)
150 assert launcher.teardown == expected_teardown
152 # Note: Testing of "globals" processing handled in launcher_parse_args_test.py
154 # Instead of just checking that the config is loaded, check that the
155 # Launcher loaded the expected types as well.
157 assert isinstance(launcher.root_environment, Environment)
158 env_config = launcher.config_loader.load_config(
159 config["environment"],
160 ConfigSchema.ENVIRONMENT,
161 )
162 assert check_class_name(launcher.root_environment, env_config["class"])
164 assert isinstance(launcher.optimizer, Optimizer)
165 if "optimizer" in config:
166 opt_config = launcher.config_loader.load_config(
167 config["optimizer"],
168 ConfigSchema.OPTIMIZER,
169 )
170 assert check_class_name(launcher.optimizer, opt_config["class"])
172 assert isinstance(launcher.storage, Storage)
173 if "storage" in config:
174 storage_config = launcher.config_loader.load_config(
175 config["storage"],
176 ConfigSchema.STORAGE,
177 )
178 assert check_class_name(launcher.storage, storage_config["class"])
180 assert isinstance(launcher.scheduler, Scheduler)
181 if "scheduler" in config:
182 scheduler_config = launcher.config_loader.load_config(
183 config["scheduler"],
184 ConfigSchema.SCHEDULER,
185 )
186 assert check_class_name(launcher.scheduler, scheduler_config["class"])
188 # TODO: Check that the launcher assigns the tunables values as expected.